"use server" import { revalidatePath } from "next/cache" import { PermissionDeniedError, requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import type { ActionState } from "@/shared/types/action-state" import { sendNotification } from "@/modules/notifications/dispatcher" import { SendMessageSchema, MessageIdSchema, UpdateNotificationPreferencesSchema, } from "./schema" import { getMessages, getMessageById, createMessage, markMessageAsRead, deleteMessage, getRecipients, getUnreadMessageCount, } from "./data-access" import { getNotifications, markNotificationAsRead, markAllNotificationsAsRead, getUnreadNotificationCount, } from "@/modules/notifications/data-access" import { getNotificationPreferences, upsertNotificationPreferences, } from "@/modules/notifications/preferences" import type { Message, MessageType, RecipientOption } from "./types" import type { Notification, NotificationPreferences, UpdateNotificationPreferencesInput, } from "@/modules/notifications/types" export async function sendMessageAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_SEND) const parsed = SendMessageSchema.safeParse({ receiverId: formData.get("receiverId"), subject: formData.get("subject") || undefined, content: formData.get("content"), parentMessageId: formData.get("parentMessageId") || undefined, }) if (!parsed.success) { return { success: false, message: "Invalid form data", errors: parsed.error.flatten().fieldErrors } } const input = parsed.data if (input.receiverId === ctx.userId) { return { success: false, message: "Cannot send a message to yourself" } } const id = await createMessage({ senderId: ctx.userId, receiverId: input.receiverId, subject: input.subject, content: input.content, parentMessageId: input.parentMessageId, }) // Notify the receiver about the new message via the notifications dispatcher. // This respects user notification preferences (SMS/WeChat/Email/In-App). await sendNotification({ userId: input.receiverId, type: "info", title: input.subject ? `New message: ${input.subject}` : "New message", content: input.content.slice(0, 200), actionUrl: `/messages/${id}`, metadata: { messageType: "message", messageId: id }, }) revalidatePath("/messages") revalidatePath(`/messages/${id}`) return { success: true, message: "Message sent", data: id } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function markMessageAsReadAction(messageId: string): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const parsed = MessageIdSchema.safeParse({ messageId }) if (!parsed.success) { return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors } } await markMessageAsRead(parsed.data.messageId, ctx.userId) revalidatePath("/messages") revalidatePath(`/messages/${parsed.data.messageId}`) return { success: true, message: "Marked as read" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function deleteMessageAction(messageId: string): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_DELETE) const parsed = MessageIdSchema.safeParse({ messageId }) if (!parsed.success) { return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors } } await deleteMessage(parsed.data.messageId, ctx.userId) revalidatePath("/messages") revalidatePath(`/messages/${parsed.data.messageId}`) return { success: true, message: "Message deleted" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getMessagesAction( params: { type: MessageType; page?: number; pageSize?: number; keyword?: string } ): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const result = await getMessages({ userId: ctx.userId, ...params }) return { success: true, data: result } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getMessageDetailAction(messageId: string): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const parsed = MessageIdSchema.safeParse({ messageId }) if (!parsed.success) { return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors } } const validMessageId = parsed.data.messageId const message = await getMessageById(validMessageId, ctx.userId) if (!message) return { success: false, message: "Message not found" } // Auto-mark as read when viewed by receiver if (!message.isRead && message.receiverId === ctx.userId) { await markMessageAsRead(validMessageId, ctx.userId) revalidatePath("/messages") } return { success: true, data: message } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getRecipientsAction(): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_SEND) const recipients = await getRecipients(ctx.userId, ctx.dataScope) return { success: true, data: recipients } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getUnreadMessageCountAction(): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const count = await getUnreadMessageCount(ctx.userId) return { success: true, data: count } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getUnreadNotificationCountAction(): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const count = await getUnreadNotificationCount(ctx.userId) return { success: true, data: count } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getNotificationsAction( params?: { page?: number; pageSize?: number; unreadOnly?: boolean } ): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const result = await getNotifications(ctx.userId, params) return { success: true, data: result } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function markNotificationAsReadAction( notificationId: string ): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) await markNotificationAsRead(notificationId, ctx.userId) revalidatePath("/messages") return { success: true, message: "Notification marked as read" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function markAllNotificationsAsReadAction(): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) await markAllNotificationsAsRead(ctx.userId) revalidatePath("/messages") return { success: true, message: "All notifications marked as read" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getNotificationPreferencesAction(): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) const prefs = await getNotificationPreferences(ctx.userId) return { success: true, data: prefs } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function updateNotificationPreferencesAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.MESSAGE_READ) // 从 FormData 中解析布尔值(checkbox 提交 "on" 或不提交) const parseBool = (key: string): boolean => formData.get(key) === "on" // 从 FormData 中解析时间字符串("HH:mm"),空字符串转为 null const parseTime = (key: string): string | null => { const v = formData.get(key) if (typeof v !== "string") return null const trimmed = v.trim() return trimmed.length > 0 ? trimmed : null } const parsed = UpdateNotificationPreferencesSchema.safeParse({ emailEnabled: parseBool("emailEnabled"), smsEnabled: parseBool("smsEnabled"), pushEnabled: parseBool("pushEnabled"), homeworkNotifications: parseBool("homeworkNotifications"), gradeNotifications: parseBool("gradeNotifications"), announcementNotifications: parseBool("announcementNotifications"), messageNotifications: parseBool("messageNotifications"), attendanceNotifications: parseBool("attendanceNotifications"), quietHoursEnabled: parseBool("quietHoursEnabled"), quietHoursStart: parseTime("quietHoursStart"), quietHoursEnd: parseTime("quietHoursEnd"), }) if (!parsed.success) { return { success: false, message: "Invalid form data", errors: parsed.error.flatten().fieldErrors } } const input: UpdateNotificationPreferencesInput = parsed.data const updated = await upsertNotificationPreferences(ctx.userId, input) if (!updated) { return { success: false, message: "Failed to update notification preferences" } } revalidatePath("/settings") return { success: true, message: "Notification preferences updated", data: updated } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } }