From 7f66f66cfac88a96be27c40cd90906d0b6d1e13c Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 18 May 2026 17:24:46 +0200 Subject: [PATCH] =?UTF-8?q?KI-AGENT:=20Matrix-R=C3=A4ume=20in=20FEDEO=20pr?= =?UTF-8?q?ovisionieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/modules/matrix.service.ts | 150 +++++++++++++++++++++++++ backend/src/routes/communication.ts | 25 +++++ frontend/pages/communication/index.vue | 114 ++++++++++++++++++- 3 files changed, 287 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index 1cd802e..4899c12 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -122,6 +122,20 @@ export function matrixService(server: FastifyInstance) { const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) => `#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}` + const tenantRoomAliasLocalpart = ( + tenant: { id: number, short?: string | null, name?: string | null }, + roomKey: string + ) => { + const tenantSeed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`) + const roomSeed = normalizeMatrixAliasSeed(roomKey) + return `fedeo_${tenantSeed}_${tenant.id}_${roomSeed}` + } + + const tenantRoomAlias = ( + tenant: { id: number, short?: string | null, name?: string | null }, + roomKey: string + ) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${serverName()}` + const buildSharedSecretMac = ( nonce: string, username: string, @@ -459,6 +473,140 @@ export function matrixService(server: FastifyInstance) { } } + const getTenantRoomStatus = async ( + tenantId: number | null, + roomKey: string, + roomName: string + ) => { + const tenant = await getCurrentTenant(tenantId) + const alias = tenantRoomAlias(tenant, roomKey) + + try { + const directoryEntry = await requestJson<{ + room_id: string + servers: string[] + }>(`${homeserverUrl()}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`) + + return { + tenantId: tenant.id, + tenantName: tenant.name, + key: roomKey, + name: roomName, + alias, + exists: true, + roomId: directoryEntry.room_id, + servers: directoryEntry.servers, + } + } catch (err: any) { + if (err.statusCode === 404 || err.errcode === "M_NOT_FOUND") { + return { + tenantId: tenant.id, + tenantName: tenant.name, + key: roomKey, + name: roomName, + alias, + exists: false, + roomId: null, + servers: [], + } + } + + throw err + } + } + + const provisionTenantRoom = async ( + userId: string, + tenantId: number | null, + options: { + key?: string + name?: string + topic?: string + } = {} + ) => { + const tenant = await getCurrentTenant(tenantId) + const key = normalizeMatrixAliasSeed(options.key || options.name || "allgemein") + const name = (options.name || "Allgemeiner Chat").trim() || "Allgemeiner Chat" + const topic = (options.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim() + const existing = await getTenantRoomStatus(tenant.id, key, name) + const userAccount = await provisionCurrentUser(userId, tenant.id) + const tenantSpace = await provisionCurrentTenantSpace(userId, tenant.id) + + if (existing.exists) { + return { + ...existing, + created: false, + alreadyExisted: true, + parentSpaceRoomId: tenantSpace.roomId, + invitedUserId: userAccount.matrixUserId, + } + } + + const serviceLogin = await ensureServiceAccessToken() + const createdRoom = await requestMatrixJson<{ room_id: string }>( + "/_matrix/client/v3/createRoom", + serviceLogin.access_token, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + topic, + preset: "private_chat", + visibility: "private", + room_alias_name: tenantRoomAliasLocalpart(tenant, key), + invite: [userAccount.matrixUserId], + initial_state: [ + { + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: "invited", + }, + }, + { + type: "m.space.parent", + state_key: tenantSpace.roomId, + content: { + via: [serverName()], + canonical: true, + }, + }, + ], + }), + } + ) + + await requestMatrixJson( + `/_matrix/client/v3/rooms/${encodeURIComponent(tenantSpace.roomId)}/state/m.space.child/${encodeURIComponent(createdRoom.room_id)}`, + serviceLogin.access_token, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + via: [serverName()], + suggested: true, + order: "10", + }), + } + ) + + return { + tenantId: tenant.id, + tenantName: tenant.name, + key, + name, + alias: tenantRoomAlias(tenant, key), + exists: true, + created: true, + alreadyExisted: false, + roomId: createdRoom.room_id, + parentSpaceRoomId: tenantSpace.roomId, + invitedUserId: userAccount.matrixUserId, + serviceUserId: serviceLogin.user_id, + } + } + return { getStatus, matrixUserIdForUser, @@ -466,5 +614,7 @@ export function matrixService(server: FastifyInstance) { provisionCurrentUser, getTenantSpaceStatus, provisionCurrentTenantSpace, + getTenantRoomStatus, + provisionTenantRoom, } } diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index 4be081c..80f6369 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -49,4 +49,29 @@ export default async function communicationRoutes(server: FastifyInstance) { .send({ error: err.message || "Matrix tenant space 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) { + req.log.error(err) + return reply + .code(err.statusCode || 500) + .send({ error: err.message || "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) { + req.log.error(err) + return reply + .code(err.statusCode || 500) + .send({ error: err.message || "Matrix room provisioning failed" }) + } + }) } diff --git a/frontend/pages/communication/index.vue b/frontend/pages/communication/index.vue index 453430e..c62976d 100644 --- a/frontend/pages/communication/index.vue +++ b/frontend/pages/communication/index.vue @@ -5,11 +5,14 @@ const { $api } = useNuxtApp() const status = ref(null) const identity = ref(null) const tenantSpace = ref(null) +const generalRoom = ref(null) const provisionResult = ref(null) const tenantSpaceProvisionResult = ref(null) +const generalRoomProvisionResult = ref(null) const loading = ref(false) const provisioning = ref(false) const tenantSpaceProvisioning = ref(false) +const generalRoomProvisioning = ref(false) const lastUpdated = ref(null) const statusItems = computed(() => [ @@ -48,15 +51,17 @@ const statusItems = computed(() => [ const loadMatrixInfo = async () => { loading.value = true try { - const [statusRes, identityRes, tenantSpaceRes] = await Promise.all([ + const [statusRes, identityRes, tenantSpaceRes, generalRoomRes] = await Promise.all([ $api("/api/communication/matrix/status"), $api("/api/communication/matrix/me"), - $api("/api/communication/matrix/tenant-space") + $api("/api/communication/matrix/tenant-space"), + $api("/api/communication/matrix/rooms/general") ]) status.value = statusRes identity.value = identityRes tenantSpace.value = tenantSpaceRes + generalRoom.value = generalRoomRes lastUpdated.value = new Date() } catch (error) { toast.add({ @@ -127,6 +132,44 @@ const provisionTenantSpace = async () => { } } +const provisionGeneralRoom = async () => { + generalRoomProvisioning.value = true + try { + const res = await $api("/api/communication/matrix/rooms/general/provision", { + method: "POST" + }) + + generalRoomProvisionResult.value = res + generalRoom.value = { + tenantId: res.tenantId, + tenantName: res.tenantName, + key: res.key, + name: res.name, + alias: res.alias, + exists: true, + roomId: res.roomId, + servers: res.servers || [] + } + tenantSpace.value = { + ...tenantSpace.value, + exists: true, + roomId: res.parentSpaceRoomId || tenantSpace.value?.roomId + } + + toast.add({ + title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt", + color: "success" + }) + } catch (error) { + toast.add({ + title: "Allgemeiner Chat konnte nicht erstellt werden", + color: "error" + }) + } finally { + generalRoomProvisioning.value = false + } +} + const formatDateTime = (value) => { if (!value) return "-" @@ -334,6 +377,73 @@ onMounted(loadMatrixInfo) + + + + +
+
+
+

+ Alias +

+

+ {{ generalRoom?.alias || "-" }} +

+
+
+

+ Status +

+ + {{ generalRoom?.exists ? "Vorhanden" : "Noch nicht erstellt" }} + +
+
+ +
+

+ Raum-ID +

+

+ {{ generalRoom?.roomId || "-" }} +

+
+ + +
+ + +