diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index 7412e94..e61fd86 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto" import { FastifyInstance } from "fastify" import multipart from "@fastify/multipart" -import { and, eq, inArray, ne } from "drizzle-orm" +import { and, desc, eq, inArray, ne } from "drizzle-orm" import { authProfiles, authTenantUsers, authUsers, notificationsItems, projects } from "../../db/schema" import { matrixService } from "../modules/matrix.service" import { NotificationService, UserDirectory } from "../modules/notification.service" @@ -98,6 +98,30 @@ export default async function communicationRoutes(server: FastifyInstance) { )) } + const getTenantUser = async (tenantId: number, userId: string): Promise => { + const [user] = await server.db + .select({ + userId: authTenantUsers.user_id, + email: authUsers.email, + firstName: authProfiles.first_name, + lastName: authProfiles.last_name, + fullName: authProfiles.full_name, + }) + .from(authTenantUsers) + .innerJoin(authUsers, eq(authUsers.id, authTenantUsers.user_id)) + .leftJoin(authProfiles, and( + eq(authProfiles.user_id, authTenantUsers.user_id), + eq(authProfiles.tenant_id, tenantId) + )) + .where(and( + eq(authTenantUsers.tenant_id, tenantId), + eq(authTenantUsers.user_id, userId) + )) + .limit(1) + + return user || null + } + const getSenderName = async (tenantId: number, senderUserId: string) => { const [sender] = await server.db .select({ @@ -195,6 +219,67 @@ export default async function communicationRoutes(server: FastifyInstance) { } } + const hasChatNotificationForMessage = async (tenantId: number, userId: string, messageId: string) => { + const rows = await server.db + .select({ + payload: notificationsItems.payload, + }) + .from(notificationsItems) + .where(and( + eq(notificationsItems.tenantId, tenantId), + eq(notificationsItems.userId, userId), + eq(notificationsItems.eventType, "communication.message.new") + )) + .orderBy(desc(notificationsItems.createdAt)) + .limit(200) + + return rows.some((row) => (row.payload as any)?.messageId === messageId) + } + + const notifyCurrentUserAboutIncomingMatrixMessages = async (req: any, room: any, messages: any[]) => { + if (!req.user.tenant_id || !messages.length) return + + try { + const currentUser = await getTenantUser(req.user.tenant_id, req.user.user_id) + if (!currentUser) return + + for (const message of messages) { + if (message.own || !message.id) continue + + const text = message.body || message.attachment?.fileName || "Neue Nachricht" + const mentioned = mentionedRecipientIds(text, [currentUser]).includes(currentUser.userId) + const direct = room?.type === "direct" + + if (!direct && !mentioned) continue + if (await hasChatNotificationForMessage(req.user.tenant_id, req.user.user_id, message.id)) continue + + const senderName = message.senderDisplayName || message.sender || "Matrix" + const preview = text.length > 160 ? `${text.slice(0, 157)}...` : text + + await notifications.trigger({ + tenantId: req.user.tenant_id, + userId: req.user.user_id, + eventType: "communication.message.new", + title: mentioned ? `${senderName} hat dich erwähnt` : `Neue Direktnachricht von ${senderName}`, + message: preview, + payload: { + link: `/communication/chat?room=${encodeURIComponent(room.key)}`, + roomKey: room.key, + roomName: room.name, + roomType: room.type, + messageId: message.id, + matrixSender: message.sender, + mentioned, + direct, + }, + channels: ["inapp", "push"], + }) + } + } catch (err) { + req.log.error({ err }, "Eingehende Matrix-Benachrichtigung konnte nicht ausgelöst werden") + } + } + const unreadChatNotifications = async (tenantId: number, userId: string) => { return await server.db .select({ @@ -739,13 +824,19 @@ export default async function communicationRoutes(server: FastifyInstance) { server.get("/communication/matrix/rooms/:roomKey/sync", async (req, reply) => { try { const query = req.query as { since?: string; initial?: string } - return await matrix.syncTenantRoomEvents( + const result = await matrix.syncTenantRoomEvents( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), query.since, query.initial === "1" ) + if (query.since && query.initial !== "1" && result.messages?.length) { + const room = await matrix.getTenantRoomStatus(req.user.tenant_id, result.key, result.name) + await notifyCurrentUserAboutIncomingMatrixMessages(req, room, result.messages) + } + + return result } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix sync failed") }