import { ActionError, defineAction } from 'astro:actions'; import { Session } from '@util/session.ts'; import { Permissions } from '@util/permissions.ts'; import { db } from '@db/database.ts'; import { z } from 'astro:schema'; import { UPLOAD_PATH } from 'astro:env/server'; import fs from 'node:fs'; import crypto from 'node:crypto'; import path from 'node:path'; import { sendWebhook, WebhookAction } from '@util/webhook.ts'; export const report = { submitReport: defineAction({ input: z.object({ urlHash: z.string(), reason: z.string(), body: z.string(), files: z.array(z.instanceof(File)).nullable() }), handler: async (input) => { const report = await db.getReportByUrlHash({ urlHash: input.urlHash }); if (!report) { throw new ActionError({ code: 'NOT_FOUND' }); } if (!UPLOAD_PATH) { throw new ActionError({ code: 'FORBIDDEN', message: 'Es dürfen keine Anhänge hochgeladen werden' }); } const filePaths = [] as string[]; try { await db.transaction(async (tx) => { for (const file of input.files ?? []) { const uuid = crypto.randomUUID(); const tmpFilePath = path.join(UPLOAD_PATH!, uuid); const tmpFileStream = fs.createWriteStream(tmpFilePath); filePaths.push(tmpFilePath); const md5Hash = crypto.createHash('md5'); for await (const chunk of file.stream()) { md5Hash.update(chunk); tmpFileStream.write(chunk); } const hash = md5Hash.digest('hex'); const filePath = path.join(UPLOAD_PATH!, hash); await tx.addReportAttachment({ type: 'video', hash: hash, reportId: report.id }); fs.renameSync(tmpFilePath, filePath); filePaths.pop(); filePaths.push(filePath); } await tx.submitReport({ urlHash: input.urlHash, reason: input.reason, body: input.body }); }); } catch (e) { for (const filePath of filePaths) { fs.rmSync(filePath); } throw e; } }, accept: 'form' }), addReport: defineAction({ input: z.object({ reason: z.string(), body: z.string().nullable(), createdAt: z.string().datetime().nullable(), reporter: z.number(), reported: z.number().nullable() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); const { id } = await db.addReport({ reason: input.reason, body: input.body, createdAt: input.createdAt ? new Date(input.createdAt) : null, reporterTeamId: input.reporter, reportedTeamId: input.reported }); return { id: id }; } }), reportStatus: defineAction({ input: z.object({ reportId: z.number() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); return { reportStatus: await db.getReportStatus(input) }; } }), editReportStatus: defineAction({ input: z.object({ reportId: z.number(), status: z.enum(['open', 'closed']).nullable(), notice: z.string().nullable(), statement: z.string().nullable(), strikeReasonId: z.number().nullable() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); let preReportStrike; if (input.status === 'closed') preReportStrike = await db.getStrikeByReportId({ reportId: input.reportId }); await db.transaction(async (tx) => { await tx.editReportStatus(input); if (input.strikeReasonId) { await db.editStrike({ reportId: input.reportId, strikeReasonId: input.strikeReasonId }); } }); if (input.status === 'closed' && preReportStrike?.strikeReasonId != input.strikeReasonId) { const report = await db.getReportById({ id: input.reportId }); if (report.reported) { const strikes = await db.getStrikesByTeamId({ teamId: report.reported.id }); const teamMembers = await db.getTeamMembersByTeamId({ teamId: report.reported.id }); // send webhook in background sendWebhook(WebhookAction.Strike, { users: teamMembers.map((tm) => tm.user.uuid!), totalWeight: strikes.map((strike) => strike.reason.weight).reduce((a, b) => a + b, 0) }); } } } }), reports: defineAction({ input: z.object({ reporter: z.string().nullish(), reported: z.string().nullish() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); return { reports: await db.getReports(input) }; } }), addStrikeReason: defineAction({ input: z.object({ name: z.string(), weight: z.number() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Admin); return await db.addStrikeReason(input); } }), editStrikeReason: defineAction({ input: z.object({ id: z.number(), name: z.string(), weight: z.number() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Admin); await db.editStrikeReason(input); } }), deleteStrikeReason: defineAction({ input: z.object({ id: z.number() }), handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Admin); await db.deleteStrikeReason(input); } }), strikeReasons: defineAction({ handler: async (_, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); return { strikeReasons: await db.getStrikeReasons({}) }; } }), teamNamesByUsername: defineAction({ input: z.object({ username: z.string().nullish() }), handler: async (input) => { const teams = await db.getTeamsByUsername({ username: input.username ?? '', limit: 5 }); return { teamNames: teams.map((team) => ({ name: team.user.username, value: team.team.name })) }; } }), teamNamesByTeamName: defineAction({ input: z.object({ teamName: z.string().nullish() }), handler: async (input) => { const teams = await db.getTeams({ name: input.teamName, limit: 5 }); return { teamNames: teams.map((team) => ({ name: team.name, value: team.name })) }; } }) };