Files
EpicNext-Cms/src/actions/social.ts
T
openhands df38dccbf1
Local Build and Deploy / deploy (push) Failing after 46s
style: format code biome
2026-07-13 21:57:41 +02:00

145 lines
4.1 KiB
TypeScript

"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<void> {
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<void> {
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`);
}