From 655459a46bee31dc3db8c705025791564a36a975 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 18 May 2026 17:41:31 +0200 Subject: [PATCH] KI-AGENT: Nativen Matrix-Chat in FEDEO starten --- backend/src/modules/matrix.service.ts | 150 +++++++++++++++++++++++ backend/src/routes/communication.ts | 23 ++++ frontend/pages/communication/index.vue | 157 ++++++++++++++++++++++--- 3 files changed, 312 insertions(+), 18 deletions(-) diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index 4899c12..3ac1e5a 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -11,6 +11,17 @@ type MatrixErrorResponse = { error?: string } +type MatrixRoomEvent = { + event_id: string + sender: string + origin_server_ts: number + type: string + content?: { + body?: string + msgtype?: string + } +} + const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "") const readLocalDevRegistrationSharedSecret = () => { if (process.env.NODE_ENV === "production") return "" @@ -277,6 +288,30 @@ export function matrixService(server: FastifyInstance) { }) } + const createAccessTokenForUser = async (userId: string, tenantId: number | null) => { + await provisionCurrentUser(userId, tenantId) + + const matrixUserId = await matrixUserIdForUser(userId, tenantId) + const serviceLogin = await ensureServiceAccessToken() + const validUntilMs = Date.now() + 10 * 60 * 1000 + + const login = await requestMatrixJson<{ access_token: string }>( + `/_synapse/admin/v1/users/${encodeURIComponent(matrixUserId)}/login`, + serviceLogin.access_token, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ valid_until_ms: validUntilMs }), + } + ) + + return { + accessToken: login.access_token, + matrixUserId, + validUntilMs, + } + } + const getStatus = async () => { const configured = Boolean(homeserverUrl() && serverName()) @@ -607,6 +642,118 @@ export function matrixService(server: FastifyInstance) { } } + const ensureCurrentUserJoinedRoom = async ( + userId: string, + tenantId: number | null, + room: { roomId: string, alias: string } + ) => { + const session = await createAccessTokenForUser(userId, tenantId) + + try { + await requestMatrixJson( + `/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`, + session.accessToken, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + } + ) + } catch (err: any) { + if (err.errcode !== "M_FORBIDDEN" && err.errcode !== "M_UNKNOWN") { + throw err + } + } + + return session + } + + const getGeneralRoomMessages = async (userId: string, tenantId: number | null) => { + const room = await provisionTenantRoom(userId, tenantId, { + key: "allgemein", + name: "Allgemeiner Chat", + }) + + const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { + roomId: room.roomId, + alias: room.alias, + }) + + const response = await requestMatrixJson<{ + chunk: MatrixRoomEvent[] + start?: string + end?: string + }>( + `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/messages?dir=b&limit=50`, + session.accessToken + ) + + return { + roomId: room.roomId, + alias: room.alias, + matrixUserId: session.matrixUserId, + messages: response.chunk + .filter((event) => event.type === "m.room.message" && event.content?.msgtype === "m.text") + .map((event) => ({ + id: event.event_id, + sender: event.sender, + body: event.content?.body || "", + timestamp: event.origin_server_ts, + own: event.sender === session.matrixUserId, + })) + .reverse(), + } + } + + const sendGeneralRoomMessage = async ( + userId: string, + tenantId: number | null, + text: string + ) => { + const message = text.trim() + + if (!message) { + throw Object.assign( + new Error("Message text is required"), + { statusCode: 400 } + ) + } + + const room = await provisionTenantRoom(userId, tenantId, { + key: "allgemein", + name: "Allgemeiner Chat", + }) + + const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { + roomId: room.roomId, + alias: room.alias, + }) + const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}` + + const response = await requestMatrixJson<{ event_id: string }>( + `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`, + session.accessToken, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + msgtype: "m.text", + body: message, + }), + } + ) + + return { + id: response.event_id, + sender: session.matrixUserId, + body: message, + timestamp: Date.now(), + own: true, + roomId: room.roomId, + alias: room.alias, + } + } + return { getStatus, matrixUserIdForUser, @@ -616,5 +763,8 @@ export function matrixService(server: FastifyInstance) { provisionCurrentTenantSpace, getTenantRoomStatus, provisionTenantRoom, + createAccessTokenForUser, + getGeneralRoomMessages, + sendGeneralRoomMessage, } } diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index 80f6369..69c1142 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -74,4 +74,27 @@ export default async function communicationRoutes(server: FastifyInstance) { .send({ error: err.message || "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) { + req.log.error(err) + return reply + .code(err.statusCode || 500) + .send({ error: err.message || "Matrix messages failed" }) + } + }) + + server.post("/communication/matrix/rooms/general/messages", async (req, reply) => { + try { + const body = req.body as { text?: string } + return await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "") + } catch (err: any) { + req.log.error(err) + return reply + .code(err.statusCode || 500) + .send({ error: err.message || "Matrix message send failed" }) + } + }) } diff --git a/frontend/pages/communication/index.vue b/frontend/pages/communication/index.vue index 1c2f182..580342d 100644 --- a/frontend/pages/communication/index.vue +++ b/frontend/pages/communication/index.vue @@ -10,10 +10,14 @@ const generalRoom = ref(null) const provisionResult = ref(null) const tenantSpaceProvisionResult = ref(null) const generalRoomProvisionResult = ref(null) +const matrixMessages = ref([]) +const matrixMessageDraft = ref("") const loading = ref(false) const provisioning = ref(false) const tenantSpaceProvisioning = ref(false) const generalRoomProvisioning = ref(false) +const matrixMessagesLoading = ref(false) +const matrixMessageSending = ref(false) const lastUpdated = ref(null) const statusItems = computed(() => [ @@ -83,6 +87,10 @@ const loadMatrixInfo = async () => { tenantSpace.value = tenantSpaceRes generalRoom.value = generalRoomRes lastUpdated.value = new Date() + + if (generalRoomRes?.exists) { + await loadGeneralMessages() + } } catch (error) { toast.add({ title: "Matrix-Status konnte nicht geladen werden", @@ -180,6 +188,8 @@ const provisionGeneralRoom = async () => { title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt", color: "success" }) + + await loadGeneralMessages() } catch (error) { toast.add({ title: "Allgemeiner Chat konnte nicht erstellt werden", @@ -190,6 +200,53 @@ const provisionGeneralRoom = async () => { } } +const loadGeneralMessages = async () => { + matrixMessagesLoading.value = true + try { + const res = await $api("/api/communication/matrix/rooms/general/messages") + matrixMessages.value = res.messages || [] + generalRoom.value = { + ...generalRoom.value, + alias: res.alias || generalRoom.value?.alias, + exists: true, + roomId: res.roomId || generalRoom.value?.roomId + } + } catch (error) { + toast.add({ + title: "Matrix-Nachrichten konnten nicht geladen werden", + color: "error" + }) + } finally { + matrixMessagesLoading.value = false + } +} + +const sendMatrixMessage = async () => { + const text = matrixMessageDraft.value.trim() + if (!text) return + + matrixMessageSending.value = true + try { + const message = await $api("/api/communication/matrix/rooms/general/messages", { + method: "POST", + body: { text } + }) + + matrixMessages.value = [ + ...matrixMessages.value, + message + ] + matrixMessageDraft.value = "" + } catch (error) { + toast.add({ + title: "Matrix-Nachricht konnte nicht gesendet werden", + color: "error" + }) + } finally { + matrixMessageSending.value = false + } +} + const formatDateTime = (value) => { if (!value) return "-" @@ -199,6 +256,15 @@ const formatDateTime = (value) => { }).format(value) } +const formatMessageTime = (timestamp) => { + if (!timestamp) return "" + + return new Intl.DateTimeFormat("de-DE", { + dateStyle: "short", + timeStyle: "short" + }).format(new Date(timestamp)) +} + onMounted(loadMatrixInfo) @@ -504,31 +570,86 @@ onMounted(loadMatrixInfo) Matrix-Kommunikation

- {{ activeMatrixTarget?.name || activeMatrixTarget?.alias || "Element Web" }} + {{ generalRoom?.name || generalRoom?.alias || "Allgemeiner Chat" }}

- - Separat öffnen - +
+ + Nachrichten laden + + + Element öffnen + +
-
-