import { createHash } from "node:crypto" import { FastifyInstance } from "fastify" import multipart from "@fastify/multipart" import { and, 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" const getUserDirectory: UserDirectory = async (server: FastifyInstance, userId) => { const rows = await server.db .select({ email: authUsers.email }) .from(authUsers) .where(eq(authUsers.id, userId)) .limit(1) return rows[0] || null } type ChatRecipient = { userId: string email?: string | null firstName?: string | null lastName?: string | null fullName?: string | null } export default async function communicationRoutes(server: FastifyInstance) { await server.register(multipart, { limits: { fileSize: 25 * 1024 * 1024 }, }) const matrix = matrixService(server) const notifications = new NotificationService(server, getUserDirectory) const handleMatrixError = (req: any, reply: any, err: any, fallbackMessage: string) => { req.log.error(err) return reply .code(err.statusCode || 500) .send({ error: err.message || fallbackMessage }) } const roomOptionsFromRequest = (req: any) => { const params = req.params as { roomKey?: string } const body = (req.body || {}) as { key?: string name?: string topic?: string type?: string entityType?: string | null entityId?: number | null entityUuid?: string | null } return { key: params.roomKey || body.key, name: body.name, topic: body.topic, type: body.type, entityType: body.entityType, entityId: body.entityId, entityUuid: body.entityUuid, } } const projectRoomKey = (projectId: number) => `project_${projectId}` const directRoomKey = (firstUserId: string, secondUserId: string) => { const hash = createHash("sha256") .update([firstUserId, secondUserId].sort().join(":")) .digest("hex") .slice(0, 16) return `direct_${hash}` } const displayUserName = (user: { fullName?: string | null; firstName?: string | null; lastName?: string | null; email?: string | null }) => { const name = user.fullName || [user.firstName, user.lastName].filter(Boolean).join(" ") return name || user.email || "Benutzer" } const getTenantRecipients = async (tenantId: number, senderUserId: string) => { return 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), ne(authTenantUsers.user_id, senderUserId) )) } const getSenderName = async (tenantId: number, senderUserId: string) => { const [sender] = await server.db .select({ email: authUsers.email, firstName: authProfiles.first_name, lastName: authProfiles.last_name, fullName: authProfiles.full_name, }) .from(authUsers) .leftJoin(authProfiles, and( eq(authProfiles.user_id, authUsers.id), eq(authProfiles.tenant_id, tenantId) )) .where(eq(authUsers.id, senderUserId)) .limit(1) return sender ? displayUserName(sender) : "FEDEO" } const mentionAliasesForUser = (user: ChatRecipient) => { const name = displayUserName(user) return Array.from(new Set([ name, user.fullName, [user.firstName, user.lastName].filter(Boolean).join(" "), user.firstName, user.email, ].filter(Boolean).map((value) => String(value).toLowerCase()))) } const mentionedRecipientIds = (text: string, recipients: ChatRecipient[]) => { const normalizedText = text.toLowerCase() return recipients .filter((recipient) => mentionAliasesForUser(recipient).some((alias) => normalizedText.includes(`@${alias}`) )) .map((recipient) => recipient.userId) } const chatMessageRecipients = async ( tenantId: number, senderUserId: string, room: any, text: string ) => { const recipients = await getTenantRecipients(tenantId, senderUserId) const mentioned = new Set(mentionedRecipientIds(text, recipients)) const directRecipients = new Set() if (room?.type === "direct" && room.entityUuid && room.entityUuid !== senderUserId) { directRecipients.add(room.entityUuid) } return recipients .filter((recipient) => directRecipients.has(recipient.userId) || mentioned.has(recipient.userId)) .map((recipient) => ({ ...recipient, mentioned: mentioned.has(recipient.userId), direct: directRecipients.has(recipient.userId), })) } const notifyUsersAboutChatMessage = async (req: any, room: any, message: any, text: string) => { if (!req.user.tenant_id) return try { const recipients = await chatMessageRecipients(req.user.tenant_id, req.user.user_id, room, text) if (!recipients.length) return const senderName = await getSenderName(req.user.tenant_id, req.user.user_id) const preview = text.length > 160 ? `${text.slice(0, 157)}...` : text for (const recipient of recipients) { await notifications.trigger({ tenantId: req.user.tenant_id, userId: recipient.userId, eventType: "communication.message.new", title: recipient.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, mentioned: recipient.mentioned, direct: recipient.direct, }, channels: ["inapp", "push"], }) } } catch (err) { req.log.error({ err }, "Chat-Benachrichtigung konnte nicht ausgelöst werden") } } const unreadChatNotifications = async (tenantId: number, userId: string) => { return await server.db .select({ id: notificationsItems.id, payload: notificationsItems.payload, }) .from(notificationsItems) .where(and( eq(notificationsItems.tenantId, tenantId), eq(notificationsItems.userId, userId), eq(notificationsItems.eventType, "communication.message.new"), eq(notificationsItems.channel, "inapp"), ne(notificationsItems.status, "read") )) } const markRoomNotificationsRead = async (tenantId: number, userId: string, roomKey: string) => { const rows = await unreadChatNotifications(tenantId, userId) const ids = rows .filter((row) => (row.payload as any)?.roomKey === roomKey) .map((row) => row.id) if (!ids.length) return { read: 0 } await server.db .update(notificationsItems) .set({ readAt: new Date(), status: "read" }) .where(and( eq(notificationsItems.tenantId, tenantId), eq(notificationsItems.userId, userId), inArray(notificationsItems.id, ids) )) return { read: ids.length } } const uploadedAttachmentFromRequest = async (req: any) => { const data = await req.file() if (!data?.file) { throw Object.assign( new Error("Keine Datei hochgeladen"), { statusCode: 400 } ) } const buffer = await data.toBuffer() return { buffer, filename: data.filename || "Anhang", mimeType: data.mimetype || "application/octet-stream", size: buffer.length, } } const callModeFromRequest = (req: any): "audio" | "video" => { const body = (req.body || {}) as { mode?: string } return body.mode === "audio" ? "audio" : "video" } const notifyTenantUsersAboutCall = async (req: any, room: { key?: string; name?: string }, mode: "audio" | "video") => { if (!req.user.tenant_id) return try { const recipientRows = await server.db .select({ userId: authTenantUsers.user_id }) .from(authTenantUsers) .where(and( eq(authTenantUsers.tenant_id, req.user.tenant_id), ne(authTenantUsers.user_id, req.user.user_id) )) const userIds = recipientRows.map((row) => row.userId) if (!userIds.length) return await notifications.trigger({ tenantId: req.user.tenant_id, userIds, eventType: "communication.call.started", title: mode === "audio" ? "Audioanruf gestartet" : "Videokonferenz gestartet", message: `${room.name || room.key || "Ein Chatraum"} hat eine laufende Besprechung.`, payload: { link: "/communication/chat", roomKey: room.key, roomName: room.name, mode, }, channels: ["inapp", "push"], }) } catch (err) { req.log.error({ err }, "Call-Benachrichtigung konnte nicht ausgelöst werden") } } server.get("/communication/matrix/status", async () => { return matrix.getStatus() }) server.get("/communication/matrix/me", async (req) => { const userId = req.user.user_id return { matrixUserId: await matrix.matrixUserIdForUser(userId, req.user.tenant_id), displayName: await matrix.getCurrentUserDisplayName(userId, req.user.tenant_id), } }) server.post("/communication/matrix/me/provision", async (req, reply) => { try { return await matrix.provisionCurrentUser(req.user.user_id, req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix provisioning failed") } }) server.get("/communication/matrix/tenant-space", async (req, reply) => { try { return await matrix.getTenantSpaceStatus(req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix tenant space status failed") } }) server.post("/communication/matrix/tenant-space/provision", async (req, reply) => { try { return await matrix.provisionCurrentTenantSpace(req.user.user_id, req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix tenant space provisioning failed") } }) server.get("/communication/matrix/rooms", async (req, reply) => { try { return await matrix.listTenantRooms(req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix rooms failed") } }) server.get("/communication/matrix/unread", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const rows = await unreadChatNotifications(req.user.tenant_id, req.user.user_id) const rooms = rows.reduce((acc: Record, row) => { const payload = row.payload as any const roomKey = payload?.roomKey if (!roomKey) return acc acc[roomKey] = acc[roomKey] || { count: 0, mentions: 0 } acc[roomKey].count += 1 if (payload.mentioned) { acc[roomKey].mentions += 1 } return acc }, {}) return { rooms } } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix unread state failed") } }) server.get("/communication/matrix/media", async (req, reply) => { try { const query = req.query as { uri?: string; name?: string } if (!query.uri) return reply.code(400).send({ error: "Matrix-Media-URI fehlt" }) const media = await matrix.getMediaContent(req.user.user_id, req.user.tenant_id, query.uri) reply.header("Content-Type", media.contentType) if (media.contentLength) reply.header("Content-Length", media.contentLength) if (query.name) { reply.header("Content-Disposition", `inline; filename="${query.name.replace(/"/g, "")}"`) } return reply.send(media.buffer) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix media failed") } }) server.get("/communication/matrix/project-rooms", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const [roomsRes, projectRows] = await Promise.all([ matrix.listTenantRooms(req.user.tenant_id), server.db .select({ id: projects.id, name: projects.name, projectNumber: projects.projectNumber, profiles: projects.profiles, }) .from(projects) .where(and( eq(projects.tenant, req.user.tenant_id), eq(projects.archived, false) )) ]) const roomsByKey = new Map((roomsRes.rooms || []).map((room: any) => [room.key, room])) return { rooms: projectRows.map((project) => { const key = projectRoomKey(project.id) const existing = roomsByKey.get(key) as any return { ...(existing || {}), key, name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name, topic: `Projektkommunikation zu ${project.name}`, type: "project", entityType: "project", entityId: project.id, exists: Boolean(existing?.exists), projectId: project.id, projectNumber: project.projectNumber, } }) } } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix project rooms failed") } }) server.post("/communication/matrix/project-rooms/:projectId/provision", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const params = req.params as { projectId: string } const projectId = Number(params.projectId) const [project] = await server.db .select() .from(projects) .where(and( eq(projects.tenant, req.user.tenant_id), eq(projects.id, projectId) )) .limit(1) if (!project) return reply.code(404).send({ error: "Projekt nicht gefunden" }) const profileIds = (project.profiles || []) as string[] const profileRows = profileIds.length ? await server.db .select({ userId: authProfiles.user_id }) .from(authProfiles) .where(and( eq(authProfiles.tenant_id, req.user.tenant_id), inArray(authProfiles.id, profileIds) )) : [] const inviteUserIds = profileRows.map((profile) => profile.userId).filter(Boolean) as string[] return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, { key: projectRoomKey(project.id), name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name, topic: `Projektkommunikation zu ${project.name}`, type: "project", entityType: "project", entityId: project.id, inviteUserIds, }) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix project room provisioning failed") } }) server.get("/communication/matrix/direct-rooms", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const [roomsRes, userRows] = await Promise.all([ matrix.listTenantRooms(req.user.tenant_id), 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, req.user.tenant_id) )) .where(and( eq(authTenantUsers.tenant_id, req.user.tenant_id), ne(authTenantUsers.user_id, req.user.user_id) )) ]) const roomsByKey = new Map((roomsRes.rooms || []).map((room: any) => [room.key, room])) return { rooms: userRows.map((user) => { const key = directRoomKey(req.user.user_id, user.userId) const existing = roomsByKey.get(key) as any const name = displayUserName(user) return { ...(existing || {}), key, name, topic: `Direktnachricht mit ${name}`, type: "direct", entityType: "user", entityUuid: user.userId, exists: Boolean(existing?.exists), userId: user.userId, email: user.email, } }) } } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix direct rooms failed") } }) server.post("/communication/matrix/direct-rooms/:userId/provision", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const params = req.params as { userId: string } const [target] = 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, req.user.tenant_id) )) .where(and( eq(authTenantUsers.tenant_id, req.user.tenant_id), eq(authTenantUsers.user_id, params.userId) )) .limit(1) if (!target || target.userId === req.user.user_id) { return reply.code(404).send({ error: "Benutzer nicht gefunden" }) } const targetName = displayUserName(target) return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, { key: directRoomKey(req.user.user_id, target.userId), name: targetName, topic: `Direktnachricht mit ${targetName}`, type: "direct", entityType: "user", entityUuid: target.userId, inviteUserIds: [target.userId], }) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix direct room provisioning failed") } }) server.post("/communication/matrix/rooms", async (req, reply) => { try { return await matrix.provisionTenantRoom( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room provisioning failed") } }) server.get("/communication/matrix/rooms/general", async (req, reply) => { try { return await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat") } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room status failed") } }) server.post("/communication/matrix/rooms/general/provision", async (req, reply) => { try { return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, { key: "allgemein", name: "Allgemeiner Chat", }) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room provisioning failed") } }) server.get("/communication/matrix/rooms/general/messages", async (req, reply) => { try { return await matrix.getGeneralRoomMessages(req.user.user_id, req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix messages failed") } }) server.get("/communication/matrix/rooms/general/members", async (req, reply) => { try { return await matrix.getGeneralRoomMembers(req.user.user_id, req.user.tenant_id) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix members failed") } }) server.get("/communication/matrix/users", async (req, reply) => { try { const users = await matrix.listTenantCommunicationUsers(req.user.tenant_id) return { users } } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix users failed") } }) server.post("/communication/matrix/rooms/general/session", async (req, reply) => { try { return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, { key: "allgemein", name: "Allgemeiner Chat", }) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix session failed") } }) server.post("/communication/matrix/rooms/general/call-session", async (req, reply) => { try { const room = { key: "allgemein", name: "Allgemeiner Chat", } const session = await matrix.createLiveKitRoomSession(req.user.user_id, req.user.tenant_id, room) await notifyTenantUsersAboutCall(req, room, callModeFromRequest(req)) return session } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix call session failed") } }) server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => { try { return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, { key: "allgemein", name: "Allgemeiner Chat", }) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix member sync failed") } }) server.post("/communication/matrix/rooms/general/messages", async (req, reply) => { try { const body = req.body as { text?: string; replyToEventId?: string } const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "", { replyToEventId: body.replyToEventId, }) const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat") await notifyUsersAboutChatMessage(req, room, message, body.text || "") return message } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix message send failed") } }) server.post("/communication/matrix/rooms/general/attachments", async (req, reply) => { try { const attachment = await uploadedAttachmentFromRequest(req) const message = await matrix.sendGeneralRoomAttachment(req.user.user_id, req.user.tenant_id, attachment) const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat") await notifyUsersAboutChatMessage(req, room, message, `Anhang: ${attachment.filename}`) return message } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix attachment send failed") } }) server.get("/communication/matrix/rooms/:roomKey", async (req, reply) => { try { const params = req.params as { roomKey: string } return await matrix.getTenantRoomStatus(req.user.tenant_id, params.roomKey) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room status failed") } }) server.post("/communication/matrix/rooms/:roomKey/provision", async (req, reply) => { try { return await matrix.provisionTenantRoom( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room provisioning failed") } }) server.post("/communication/matrix/rooms/:roomKey/read", async (req, reply) => { try { if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" }) const params = req.params as { roomKey: string } const body = (req.body || {}) as { eventId?: string } const result = await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey) if (body.eventId) { await matrix.markTenantRoomRead(req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.eventId) } return result } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix room read state failed") } }) server.get("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => { try { return await matrix.getTenantRoomMessages( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix messages failed") } }) server.get("/communication/matrix/rooms/:roomKey/search", async (req, reply) => { try { const query = req.query as { q?: string } return await matrix.searchTenantRoomMessages( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), query.q || "" ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix search failed") } }) server.get("/communication/matrix/rooms/:roomKey/sync", async (req, reply) => { try { const query = req.query as { since?: string; initial?: string } return await matrix.syncTenantRoomEvents( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), query.since, query.initial === "1" ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix sync failed") } }) server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => { try { return await matrix.getTenantRoomMembers( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix members failed") } }) server.post("/communication/matrix/rooms/:roomKey/members/invite", async (req, reply) => { try { const body = req.body as { userId?: string } return await matrix.inviteTenantRoomMember( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.userId || "" ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix member invite failed") } }) server.delete("/communication/matrix/rooms/:roomKey/members/:matrixUserId", async (req, reply) => { try { const params = req.params as { matrixUserId: string } return await matrix.removeTenantRoomMember( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), params.matrixUserId ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix member remove failed") } }) server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => { try { return await matrix.createElementRoomSession( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix session failed") } }) server.post("/communication/matrix/rooms/:roomKey/call-session", async (req, reply) => { try { const room = roomOptionsFromRequest(req) const session = await matrix.createLiveKitRoomSession( req.user.user_id, req.user.tenant_id, room ) await notifyTenantUsersAboutCall(req, room, callModeFromRequest(req)) return session } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix call session failed") } }) server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => { try { return await matrix.syncTenantRoomMembers( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req) ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix member sync failed") } }) server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => { try { const body = req.body as { text?: string; replyToEventId?: string } const message = await matrix.sendTenantRoomMessage( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.text || "", { replyToEventId: body.replyToEventId, } ) const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key) await notifyUsersAboutChatMessage(req, room, message, body.text || "") return message } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix message send failed") } }) server.post("/communication/matrix/rooms/:roomKey/messages/:eventId/reactions", async (req, reply) => { try { const params = req.params as { eventId: string } const body = req.body as { key?: string } return await matrix.sendTenantRoomReaction( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), params.eventId, body.key || "" ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix reaction send failed") } }) server.put("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => { try { const params = req.params as { eventId: string } const body = req.body as { text?: string } return await matrix.editTenantRoomMessage( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), params.eventId, body.text || "" ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix message edit failed") } }) server.delete("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => { try { const params = req.params as { eventId: string } return await matrix.redactTenantRoomMessage( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), params.eventId ) } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix message delete failed") } }) server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => { try { const attachment = await uploadedAttachmentFromRequest(req) const message = await matrix.sendTenantRoomAttachment( req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), attachment ) const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key) await notifyUsersAboutChatMessage(req, room, message, `Anhang: ${attachment.filename}`) return message } catch (err: any) { return handleMatrixError(req, reply, err, "Matrix attachment send failed") } }) }