Files
FEDEO/backend/src/routes/communication.ts

916 lines
36 KiB
TypeScript

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<string>()
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<string, { count: number; mentions: number }>, 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")
}
})
}