"use server"; import { revalidatePath } from "next/cache"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; // Guild forum subjects are VARCHAR(255); the comment/message body lives in // guilds_forums_comments.message which is TEXT. Keep the first post's message // bounded defensively even though the column is large. const SUBJECT_MAX = 255; const MESSAGE_MAX = 10000; /** * Send a friend request to another user. * * The REQUESTER (user_from_id) is re-read from the session via auth() and is * never trusted from the submitted FormData, so a crafted form cannot send a * request "from" someone else. Only the TARGET user id is taken from the form. * * Writes into messenger_friendrequests (userFromId = requester, userToId = * target). The emulator surfaces the pending request in the in-game messenger. */ export async function sendFriendRequest(formData: FormData): Promise { const session = await auth(); const fromId = Number(session?.user?.id); if (!Number.isInteger(fromId) || fromId <= 0) return; const toId = Number(formData.get("userId")); if (!Number.isInteger(toId) || toId <= 0) return; // Can't befriend yourself. if (toId === fromId) return; try { // Guard against duplicate pending requests and already-existing friendships. const [existingRequest, existingFriendship] = await Promise.all([ prisma.messengerFriendrequests.findFirst({ where: { userFromId: fromId, userToId: toId }, select: { id: true }, }), prisma.messengerFriendships.findFirst({ where: { OR: [ { userOneId: fromId, userTwoId: toId }, { userOneId: toId, userTwoId: fromId }, ], }, select: { id: true }, }), ]); if (existingRequest || existingFriendship) return; await prisma.messengerFriendrequests.create({ data: { userFromId: fromId, userToId: toId }, }); } catch { // DB unavailable — fail soft; nothing to persist. return; } // Optional: revalidate the target profile if a username was supplied, purely // to refresh any request-state UI rendered there. const username = String(formData.get("username") ?? "") .normalize("NFC") .trim(); if (username) revalidatePath(`/u/${username}`); } /** * Open a new thread in a guild's forum. * * The AUTHOR (opener_id) is re-read from the session via auth() and is never * trusted from the submitted FormData. Only the guild id, subject, and message * come from the form. * * AtomCMS/Arcturus splits a thread into a header row (guilds_forums_threads) * plus the opening post stored as the first comment (guilds_forums_comments). * We create both in a transaction so the thread always has its first post, then * stamp posts_count = 1 to match the emulator's bookkeeping. */ export async function postThread(formData: FormData): Promise { const session = await auth(); const openerId = Number(session?.user?.id); if (!Number.isInteger(openerId) || openerId <= 0) return; const guildId = Number(formData.get("guildId")); if (!Number.isInteger(guildId) || guildId <= 0) return; const subject = String(formData.get("subject") ?? "") .normalize("NFC") .trim() .slice(0, SUBJECT_MAX); const message = String(formData.get("message") ?? "") .normalize("NFC") .trim() .slice(0, MESSAGE_MAX); if (!subject || !message) return; const now = Math.floor(Date.now() / 1000); try { // Confirm the guild exists (and has a forum) before opening a thread. const guild = await prisma.guilds.findUnique({ where: { id: guildId }, select: { id: true }, }); if (!guild) return; await prisma.$transaction(async (tx) => { const thread = await tx.guildsForumsThreads.create({ data: { guildId, openerId, subject, postsCount: 1, createdAt: now, updatedAt: now, state: 0, pinned: 0, locked: 0, adminId: 0, }, select: { id: true }, }); await tx.guildsForumsComments.create({ data: { threadId: thread.id, userId: openerId, message, createdAt: now, state: 0, adminId: 0, }, }); }); } catch { // DB unavailable — fail soft; nothing to persist. return; } revalidatePath(`/guilds/${guildId}/forum`); }