diff --git a/website/src/lib/server/moderation.ts b/website/src/lib/server/moderation.ts new file mode 100644 index 0000000..60865f5 --- /dev/null +++ b/website/src/lib/server/moderation.ts @@ -0,0 +1,22 @@ +export async function isNameAppropriate(name: string): Promise { + try { + const response = await fetch('http://localhost:9999', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name }), + }); + + if (!response.ok) { + console.error('Moderation service error:', response.status, response.statusText); + return true; + } + + const result = await response.json(); + return result.appropriate !== false; + } catch (error) { + console.error('Failed to check name with moderation service:', error); + return true; + } +} diff --git a/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts b/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts index ce5fc95..995c01a 100644 --- a/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts +++ b/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts @@ -4,6 +4,7 @@ import { db } from '$lib/server/db'; import { comment, coin, user, commentLike } from '$lib/server/db/schema'; import { eq, and, desc, sql } from 'drizzle-orm'; import { redis } from '$lib/server/redis'; +import { isNameAppropriate } from '$lib/server/moderation'; export async function GET({ params, request }) { const session = await auth.api.getSession({ @@ -73,6 +74,10 @@ export async function POST({ request, params }) { throw error(400, 'Comment must be 500 characters or less'); } + if (!(await isNameAppropriate(content.trim()))) { + throw error(400, 'Comment contains inappropriate content'); + } + const normalizedSymbol = coinSymbol.toUpperCase(); const userId = Number(session.user.id); diff --git a/website/src/routes/api/coin/create/+server.ts b/website/src/routes/api/coin/create/+server.ts index e7e497f..407aa5a 100644 --- a/website/src/routes/api/coin/create/+server.ts +++ b/website/src/routes/api/coin/create/+server.ts @@ -5,8 +5,9 @@ import { coin, userPortfolio, user, priceHistory, transaction } from '$lib/serve import { eq } from 'drizzle-orm'; import { uploadCoinIcon } from '$lib/server/s3'; import { CREATION_FEE, FIXED_SUPPLY, STARTING_PRICE, INITIAL_LIQUIDITY, TOTAL_COST, MAX_FILE_SIZE } from '$lib/data/constants'; +import { isNameAppropriate } from '$lib/server/moderation'; -function validateInputs(name: string, symbol: string, iconFile: File | null) { +async function validateInputs(name: string, symbol: string, iconFile: File | null) { if (!name || name.length < 2 || name.length > 255) { throw error(400, 'Name must be between 2 and 255 characters'); } @@ -15,6 +16,16 @@ function validateInputs(name: string, symbol: string, iconFile: File | null) { throw error(400, 'Symbol must be between 2 and 10 characters'); } + const nameAppropriate = await isNameAppropriate(name); + if (!nameAppropriate) { + throw error(400, 'Coin name contains inappropriate content'); + } + + const symbolAppropriate = await isNameAppropriate(symbol); + if (!symbolAppropriate) { + throw error(400, 'Coin symbol contains inappropriate content'); + } + if (iconFile && iconFile.size > MAX_FILE_SIZE) { throw error(400, 'Icon file must be smaller than 1MB'); } @@ -77,7 +88,7 @@ export async function POST({ request }) { const normalizedSymbol = symbol?.toUpperCase(); const userId = Number(session.user.id); - validateInputs(name, normalizedSymbol, iconFile); + await validateInputs(name, normalizedSymbol, iconFile); const [currentBalance] = await Promise.all([ validateUserBalance(userId), diff --git a/website/src/routes/api/hopium/questions/create/+server.ts b/website/src/routes/api/hopium/questions/create/+server.ts index efd072c..1ec4fd3 100644 --- a/website/src/routes/api/hopium/questions/create/+server.ts +++ b/website/src/routes/api/hopium/questions/create/+server.ts @@ -4,6 +4,7 @@ import { db } from '$lib/server/db'; import { user, predictionQuestion } from '$lib/server/db/schema'; import { eq, and, gte, count } from 'drizzle-orm'; import { validateQuestion } from '$lib/server/ai'; +import { isNameAppropriate } from '$lib/server/moderation'; import type { RequestHandler } from './$types'; const MIN_BALANCE_REQUIRED = 100000; // $100k @@ -22,6 +23,10 @@ export const POST: RequestHandler = async ({ request }) => { return json({ error: 'Question must be between 10 and 200 characters' }, { status: 400 }); } + if (!(await isNameAppropriate(cleaned))) { + return json({ error: 'Question contains inappropriate content' }, { status: 400 }); + } + const userId = Number(session.user.id); const now = new Date(); diff --git a/website/src/routes/api/settings/+server.ts b/website/src/routes/api/settings/+server.ts index f1cd4a9..d5ca308 100644 --- a/website/src/routes/api/settings/+server.ts +++ b/website/src/routes/api/settings/+server.ts @@ -5,12 +5,17 @@ import { db } from '$lib/server/db'; import { user } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; import { MAX_FILE_SIZE } from '$lib/data/constants'; +import { isNameAppropriate } from '$lib/server/moderation'; -function validateInputs(name: string, bio: string, username: string, avatarFile: File | null) { +async function validateInputs(name: string, bio: string, username: string, avatarFile: File | null) { if (name && name.length < 1) { throw error(400, 'Name cannot be empty'); } + if (name && !(await isNameAppropriate(name))) { + throw error(400, 'Name contains inappropriate content'); + } + if (bio && bio.length > 160) { throw error(400, 'Bio must be 160 characters or less'); } @@ -19,6 +24,10 @@ function validateInputs(name: string, bio: string, username: string, avatarFile: throw error(400, 'Username must be between 3 and 30 characters'); } + if (username && !(await isNameAppropriate(username))) { + throw error(400, 'Username contains inappropriate content'); + } + if (avatarFile && avatarFile.size > MAX_FILE_SIZE) { throw error(400, 'Avatar file must be smaller than 1MB'); } @@ -39,7 +48,7 @@ export async function POST({ request }) { const username = formData.get('username') as string; const avatarFile = formData.get('avatar') as File | null; - validateInputs(name, bio, username, avatarFile); + await validateInputs(name, bio, username, avatarFile); const updates: Record = { name, diff --git a/website/src/routes/api/settings/check-username/+server.ts b/website/src/routes/api/settings/check-username/+server.ts index f03f501..135fafc 100644 --- a/website/src/routes/api/settings/check-username/+server.ts +++ b/website/src/routes/api/settings/check-username/+server.ts @@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { user } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; +import { isNameAppropriate } from '$lib/server/moderation'; export async function GET({ url }) { const username = url.searchParams.get('username'); @@ -9,6 +10,10 @@ export async function GET({ url }) { return json({ available: false }); } + if (!(await isNameAppropriate(username))) { + return json({ available: false, reason: 'Inappropriate content' }); + } + const exists = await db.query.user.findFirst({ where: eq(user.username, username) });