import { NextRequest, NextResponse } from "next/server"; import { readFileSync, existsSync } from "fs"; import { auth } from "@/lib/auth"; import { sendCommand } from "@/lib/rcon"; export const dynamic = "force-dynamic"; const LOG_FILE = "/home/minecraft/server/logs/latest.log"; type ChatMessage = { time: string; type: "chat" | "join" | "leave" | "death" | "server"; player: string; message: string; }; function parseLogLine(line: string): ChatMessage | null { // [HH:MM:SS] [Server thread/INFO] [minecraft/DedicatedServer]: message const chatMatch = line.match( /\[(\d{2}:\d{2}:\d{2})\].*\[minecraft\/(?:DedicatedServer|MinecraftServer)\]:\s*<(\w+)>\s*(.*)/ ); if (chatMatch) { return { time: chatMatch[1], type: "chat", player: chatMatch[2], message: chatMatch[3] }; } // Player joins const joinMatch = line.match( /\[(\d{2}:\d{2}:\d{2})\].*\[minecraft\/(?:PlayerList|ServerPlayer)\]:\s*(\w+)\s+joined the game/ ); if (joinMatch) { return { time: joinMatch[1], type: "join", player: joinMatch[2], message: "joined the game" }; } // Player leaves const leaveMatch = line.match( /\[(\d{2}:\d{2}:\d{2})\].*\[minecraft\/(?:PlayerList|ServerPlayer)\]:\s*(\w+)\s+left the game/ ); if (leaveMatch) { return { time: leaveMatch[1], type: "leave", player: leaveMatch[2], message: "left the game" }; } // Deaths const deathMatch = line.match( /\[(\d{2}:\d{2}:\d{2})\].*\[minecraft\/(?:DedicatedServer|MinecraftServer)\]:\s*(\w+)\s+(was |died|drowned|burned|fell|starved|suffocated|hit|blew|withered|tried|experienced|went|walked|froze|was prick|was stung|was impaled|was squashed|was skewered|was squished|was pummeled|discovered)(.*)/ ); if (deathMatch) { return { time: deathMatch[1], type: "death", player: deathMatch[2], message: deathMatch[3] + (deathMatch[4] || ""), }; } // Server say command const sayMatch = line.match( /\[(\d{2}:\d{2}:\d{2})\].*\[minecraft\/(?:DedicatedServer|MinecraftServer)\]:\s*\[Server\]\s*(.*)/ ); if (sayMatch) { return { time: sayMatch[1], type: "server", player: "Server", message: sayMatch[2] }; } return null; } export async function GET(req: NextRequest) { const session = await auth(); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } if (!existsSync(LOG_FILE)) { return NextResponse.json([]); } const maxLines = parseInt(req.nextUrl.searchParams.get("lines") || "100"); try { const content = readFileSync(LOG_FILE, "utf8"); const lines = content.split("\n"); const messages: ChatMessage[] = []; // Parse from the end, collect up to maxLines relevant messages for (let i = lines.length - 1; i >= 0 && messages.length < maxLines; i--) { const msg = parseLogLine(lines[i]); if (msg) messages.unshift(msg); } return NextResponse.json(messages); } catch (e) { return NextResponse.json( { error: (e as Error).message }, { status: 500 } ); } } export async function POST(req: NextRequest) { const session = await auth(); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } const { message } = await req.json(); if (!message || typeof message !== "string" || message.length > 256) { return NextResponse.json({ error: "Invalid message" }, { status: 400 }); } // Sanitize: strip newlines/carriage returns to prevent RCON command injection const sanitized = message.replace(/[\r\n]/g, "").trim(); if (!sanitized) { return NextResponse.json({ error: "Empty message" }, { status: 400 }); } try { const response = await sendCommand(`say ${sanitized}`); return NextResponse.json({ ok: true, response }); } catch (e) { return NextResponse.json( { error: (e as Error).message }, { status: 500 } ); } }