From 248da3412c512e55da6a9b1eefd478df2c36133b Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 18 May 2026 19:15:36 +0200 Subject: [PATCH] KI-AGENT: LiveKit-Calls nativ in FEDEO integrieren --- backend/src/modules/matrix.service.ts | 75 +++++++ backend/src/routes/communication.ts | 23 ++ backend/src/utils/secrets.ts | 4 + docker-compose.yml | 4 +- frontend/package-lock.json | 119 +++++++++- frontend/package.json | 1 + frontend/pages/communication/chat.vue | 308 +++++++++++++++++++++----- 7 files changed, 480 insertions(+), 54 deletions(-) diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index 93a2d8b..b0af503 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -5,6 +5,7 @@ import { FastifyInstance } from "fastify" import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema" import { and, eq } from "drizzle-orm" import { secrets } from "../utils/secrets" +import jwt from "jsonwebtoken" type MatrixErrorResponse = { errcode?: string @@ -40,6 +41,13 @@ type MatrixLoginTokenResponse = { expires_in_ms: number } +type LiveKitGrant = { + roomJoin: boolean + room: string + canPublish: boolean + canSubscribe: boolean +} + type MatrixTenantRoomOptions = { key?: string name?: string @@ -153,6 +161,16 @@ export function matrixService(server: FastifyInstance) { ? `wss://${rtcHost()}/livekit/sfu` : `ws://localhost:${process.env.MATRIX_DEV_LIVEKIT_PORT || "7880"}`) + const livekitKey = () => + process.env.LIVEKIT_KEY || + secrets.LIVEKIT_KEY || + (process.env.NODE_ENV === "production" ? "" : "devkey") + + const livekitSecret = () => + process.env.LIVEKIT_SECRET || + secrets.LIVEKIT_SECRET || + (process.env.NODE_ENV === "production" ? "" : "devsecret-local-matrix-stack-32-chars") + const serviceUserLocalpart = () => process.env.MATRIX_SERVICE_USER_LOCALPART || secrets.MATRIX_SERVICE_USER_LOCALPART || @@ -1185,6 +1203,62 @@ export function matrixService(server: FastifyInstance) { } } + const createLiveKitRoomSession = async ( + userId: string, + tenantId: number | null, + options: MatrixTenantRoomOptions = {} + ) => { + if (!livekitKey() || !livekitSecret()) { + throw Object.assign( + new Error("LIVEKIT_KEY and LIVEKIT_SECRET are not configured"), + { statusCode: 503 } + ) + } + + const room = await provisionTenantRoom(userId, tenantId, options) + const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { + roomId: room.roomId, + alias: room.alias, + }) + const displayName = await getCurrentUserDisplayName(userId, tenantId) + const liveKitRoomName = `fedeo-${tenantId || "global"}-${room.key}`.replace(/[^a-zA-Z0-9_-]/g, "_") + const now = Math.floor(Date.now() / 1000) + const expiresInSeconds = 60 * 60 + const video: LiveKitGrant = { + roomJoin: true, + room: liveKitRoomName, + canPublish: true, + canSubscribe: true, + } + const token = jwt.sign( + { + sub: session.matrixUserId, + name: displayName, + video, + nbf: now - 10, + exp: now + expiresInSeconds, + }, + livekitSecret(), + { + algorithm: "HS256", + issuer: livekitKey(), + } + ) + + return { + roomId: room.roomId, + alias: room.alias, + key: room.key, + name: room.name, + matrixUserId: session.matrixUserId, + displayName, + liveKitUrl: livekitUrl(), + liveKitRoomName, + liveKitToken: token, + expiresInMs: expiresInSeconds * 1000, + } + } + const listTenantCommunicationUsers = async (tenantId: number | null) => { const tenant = await getCurrentTenant(tenantId) const rows = await server.db @@ -1342,6 +1416,7 @@ export function matrixService(server: FastifyInstance) { getTenantRoomMembers, sendTenantRoomMessage, createElementRoomSession, + createLiveKitRoomSession, syncTenantRoomMembers, getGeneralRoomMessages, getGeneralRoomMembers, diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index e878624..a090f59 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -136,6 +136,17 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/general/call-session", async (req, reply) => { + try { + return await matrix.createLiveKitRoomSession(req.user.user_id, req.user.tenant_id, { + key: "allgemein", + name: "Allgemeiner Chat", + }) + } 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, { @@ -213,6 +224,18 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/:roomKey/call-session", async (req, reply) => { + try { + return await matrix.createLiveKitRoomSession( + req.user.user_id, + req.user.tenant_id, + roomOptionsFromRequest(req) + ) + } 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( diff --git a/backend/src/utils/secrets.ts b/backend/src/utils/secrets.ts index e066d5a..51e758d 100644 --- a/backend/src/utils/secrets.ts +++ b/backend/src/utils/secrets.ts @@ -45,6 +45,8 @@ export let secrets = { MATRIX_LIVEKIT_URL?: string MATRIX_REGISTRATION_SHARED_SECRET?: string MATRIX_SERVICE_USER_LOCALPART?: string + LIVEKIT_KEY?: string + LIVEKIT_SECRET?: string } const secretKeys = [ @@ -84,6 +86,8 @@ const secretKeys = [ "MATRIX_LIVEKIT_URL", "MATRIX_REGISTRATION_SHARED_SECRET", "MATRIX_SERVICE_USER_LOCALPART", + "LIVEKIT_KEY", + "LIVEKIT_SECRET", ] as const const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"]) diff --git a/docker-compose.yml b/docker-compose.yml index a32bc74..5c7b1ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -200,7 +200,7 @@ services: keys: ${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace} room: - auto_create: false + auto_create: true EOF exec /livekit-server --config /tmp/livekit.yaml ports: @@ -334,7 +334,7 @@ services: keys: devkey: devsecret-local-matrix-stack-32-chars room: - auto_create: false + auto_create: true EOF exec /livekit-server --config /tmp/livekit.yaml ports: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3e8892..a7ff852 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -74,6 +74,7 @@ "image-js": "^1.1.0", "leaflet": "^1.9.4", "license-checker": "^25.0.1", + "livekit-client": "^2.19.0", "maplibre-gl": "^4.7.0", "nuxt-editorjs": "^1.0.4", "nuxt-viewport": "^2.0.6", @@ -1865,6 +1866,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@capacitor-community/bluetooth-le": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.0.tgz", @@ -3159,6 +3166,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.45.8", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.45.8.tgz", + "integrity": "sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@mapbox/geojson-rewind": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", @@ -8696,6 +8718,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -12191,7 +12220,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -14209,6 +14237,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -14784,6 +14821,26 @@ "dev": true, "license": "MIT" }, + "node_modules/livekit-client": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.0.tgz", + "integrity": "sha512-aolY1XDAtx0nHKBNm29W9OhzBnSz1CP5kq3phvRhFfi1NbvMXs8tcACjAkZTnIKgihkp+BiJScZZ3tZv0Gz8sA==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.45.8", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "9.0.5" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, "node_modules/local-pkg": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", @@ -14858,6 +14915,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -18452,6 +18522,16 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -18609,6 +18689,21 @@ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "license": "MIT" }, + "node_modules/sdp": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz", + "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -20164,6 +20259,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -21718,6 +21822,19 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/webrtc-adapter": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz", + "integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 39dea64..0be2b2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -87,6 +87,7 @@ "image-js": "^1.1.0", "leaflet": "^1.9.4", "license-checker": "^25.0.1", + "livekit-client": "^2.19.0", "maplibre-gl": "^4.7.0", "nuxt-editorjs": "^1.0.4", "nuxt-viewport": "^2.0.6", diff --git a/frontend/pages/communication/chat.vue b/frontend/pages/communication/chat.vue index 5c742f9..a50c63b 100644 --- a/frontend/pages/communication/chat.vue +++ b/frontend/pages/communication/chat.vue @@ -1,7 +1,8 @@