feat: moderation
This commit is contained in:
parent
db9c133bbb
commit
2def8d7a00
6 changed files with 61 additions and 4 deletions
22
website/src/lib/server/moderation.ts
Normal file
22
website/src/lib/server/moderation.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
export async function isNameAppropriate(name: string): Promise<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import { db } from '$lib/server/db';
|
||||||
import { comment, coin, user, commentLike } from '$lib/server/db/schema';
|
import { comment, coin, user, commentLike } from '$lib/server/db/schema';
|
||||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||||
import { redis } from '$lib/server/redis';
|
import { redis } from '$lib/server/redis';
|
||||||
|
import { isNameAppropriate } from '$lib/server/moderation';
|
||||||
|
|
||||||
export async function GET({ params, request }) {
|
export async function GET({ params, request }) {
|
||||||
const session = await auth.api.getSession({
|
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');
|
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 normalizedSymbol = coinSymbol.toUpperCase();
|
||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import { coin, userPortfolio, user, priceHistory, transaction } from '$lib/serve
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { uploadCoinIcon } from '$lib/server/s3';
|
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 { 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) {
|
if (!name || name.length < 2 || name.length > 255) {
|
||||||
throw error(400, 'Name must be between 2 and 255 characters');
|
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');
|
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) {
|
if (iconFile && iconFile.size > MAX_FILE_SIZE) {
|
||||||
throw error(400, 'Icon file must be smaller than 1MB');
|
throw error(400, 'Icon file must be smaller than 1MB');
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +88,7 @@ export async function POST({ request }) {
|
||||||
const normalizedSymbol = symbol?.toUpperCase();
|
const normalizedSymbol = symbol?.toUpperCase();
|
||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
validateInputs(name, normalizedSymbol, iconFile);
|
await validateInputs(name, normalizedSymbol, iconFile);
|
||||||
|
|
||||||
const [currentBalance] = await Promise.all([
|
const [currentBalance] = await Promise.all([
|
||||||
validateUserBalance(userId),
|
validateUserBalance(userId),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { db } from '$lib/server/db';
|
||||||
import { user, predictionQuestion } from '$lib/server/db/schema';
|
import { user, predictionQuestion } from '$lib/server/db/schema';
|
||||||
import { eq, and, gte, count } from 'drizzle-orm';
|
import { eq, and, gte, count } from 'drizzle-orm';
|
||||||
import { validateQuestion } from '$lib/server/ai';
|
import { validateQuestion } from '$lib/server/ai';
|
||||||
|
import { isNameAppropriate } from '$lib/server/moderation';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
const MIN_BALANCE_REQUIRED = 100000; // $100k
|
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 });
|
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 userId = Number(session.user.id);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@ import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { MAX_FILE_SIZE } from '$lib/data/constants';
|
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) {
|
if (name && name.length < 1) {
|
||||||
throw error(400, 'Name cannot be empty');
|
throw error(400, 'Name cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name && !(await isNameAppropriate(name))) {
|
||||||
|
throw error(400, 'Name contains inappropriate content');
|
||||||
|
}
|
||||||
|
|
||||||
if (bio && bio.length > 160) {
|
if (bio && bio.length > 160) {
|
||||||
throw error(400, 'Bio must be 160 characters or less');
|
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');
|
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) {
|
if (avatarFile && avatarFile.size > MAX_FILE_SIZE) {
|
||||||
throw error(400, 'Avatar file must be smaller than 1MB');
|
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 username = formData.get('username') as string;
|
||||||
const avatarFile = formData.get('avatar') as File | null;
|
const avatarFile = formData.get('avatar') as File | null;
|
||||||
|
|
||||||
validateInputs(name, bio, username, avatarFile);
|
await validateInputs(name, bio, username, avatarFile);
|
||||||
|
|
||||||
const updates: Record<string, any> = {
|
const updates: Record<string, any> = {
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { isNameAppropriate } from '$lib/server/moderation';
|
||||||
|
|
||||||
export async function GET({ url }) {
|
export async function GET({ url }) {
|
||||||
const username = url.searchParams.get('username');
|
const username = url.searchParams.get('username');
|
||||||
|
|
@ -9,6 +10,10 @@ export async function GET({ url }) {
|
||||||
return json({ available: false });
|
return json({ available: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await isNameAppropriate(username))) {
|
||||||
|
return json({ available: false, reason: 'Inappropriate content' });
|
||||||
|
}
|
||||||
|
|
||||||
const exists = await db.query.user.findFirst({
|
const exists = await db.query.user.findFirst({
|
||||||
where: eq(user.username, username)
|
where: eq(user.username, username)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Reference in a new issue