KI-AGENT: Matrix-Teilnehmer synchronisieren

This commit is contained in:
2026-05-18 18:33:17 +02:00
parent bb54a8779e
commit f6dd37b458
3 changed files with 199 additions and 24 deletions

View File

@@ -2,7 +2,7 @@ import { createHash, createHmac, randomBytes } from "node:crypto"
import { existsSync, readFileSync } from "node:fs"
import { resolve } from "node:path"
import { FastifyInstance } from "fastify"
import { authProfiles, authUsers, communicationRooms, tenants } from "../../db/schema"
import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema"
import { and, eq } from "drizzle-orm"
import { secrets } from "../utils/secrets"
@@ -944,21 +944,15 @@ export function matrixService(server: FastifyInstance) {
return session
}
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
await requestMatrixJson(
`/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`,
session.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}
}
)
matrixJoinedRoomCache.set(joinCacheKey, Date.now() + 30 * 60 * 1000)
return session
@@ -1112,6 +1106,125 @@ export function matrixService(server: FastifyInstance) {
}
}
const listTenantCommunicationUsers = async (tenantId: number | null) => {
const tenant = await getCurrentTenant(tenantId)
const rows = await server.db
.select({
userId: authTenantUsers.user_id,
email: authUsers.email,
profileActive: authProfiles.active,
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, tenant.id)
))
.where(eq(authTenantUsers.tenant_id, tenant.id))
return rows
.filter((row) => row.profileActive !== false)
.map((row) => ({
userId: row.userId,
email: row.email,
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
}))
}
const inviteMatrixUserToRoom = async (
room: { roomId: string },
matrixUserId: string,
reason?: string
) => {
const serviceLogin = await ensureServiceAccessToken()
try {
await requestMatrixJson(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/invite`,
serviceLogin.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: matrixUserId,
reason,
}),
}
)
return "invited"
} catch (err: any) {
if (
err.errcode === "M_FORBIDDEN" ||
err.errcode === "M_BAD_STATE" ||
err.errcode === "M_UNKNOWN"
) {
return "already_available"
}
throw err
}
}
const syncTenantRoomMembers = async (
requestingUserId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {}
) => {
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
const users = await listTenantCommunicationUsers(tenantId)
const results = []
for (const user of users) {
try {
const account = await provisionCurrentUser(user.userId, tenantId)
await inviteMatrixUserToRoom(
{ roomId: room.roomId },
account.matrixUserId,
`FEDEO-Raumsynchronisation: ${room.name}`
)
await ensureCurrentUserJoinedRoom(user.userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
results.push({
userId: user.userId,
email: user.email,
displayName: user.displayName,
matrixUserId: account.matrixUserId,
status: "joined",
ok: true,
})
} catch (err: any) {
results.push({
userId: user.userId,
email: user.email,
displayName: user.displayName,
status: "failed",
ok: false,
error: err.message || "Synchronisation fehlgeschlagen",
})
}
}
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
total: results.length,
joined: results.filter((item) => item.status === "joined").length,
invited: results.filter((item) => item.status === "invited").length,
alreadyAvailable: results.filter((item) => item.status === "already_available").length,
failed: results.filter((item) => !item.ok).length,
results,
}
}
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
getTenantRoomMessages(userId, tenantId, {
key: "allgemein",
@@ -1149,6 +1262,7 @@ export function matrixService(server: FastifyInstance) {
getTenantRoomMessages,
getTenantRoomMembers,
sendTenantRoomMessage,
syncTenantRoomMembers,
getGeneralRoomMessages,
getGeneralRoomMembers,
sendGeneralRoomMessage,

View File

@@ -125,6 +125,17 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
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 }
@@ -179,6 +190,18 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
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 }

View File

@@ -19,6 +19,7 @@ const roomCreateForm = ref({
const loading = ref(false)
const roomProvisioning = ref(false)
const roomCreating = ref(false)
const roomMembersSyncing = ref(false)
const matrixMessagesLoading = ref(false)
const matrixMembersLoading = ref(false)
const matrixMessageSending = ref(false)
@@ -288,6 +289,32 @@ const loadRoomMembers = async ({ silent = false } = {}) => {
}
}
const syncRoomMembers = async () => {
if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
roomMembersSyncing.value = true
try {
const res = await $api(`${activeRoomEndpoint.value}/members/sync`, {
method: "POST"
})
toast.add({
title: "Teilnehmer synchronisiert",
description: `${res.joined || 0} synchronisiert, ${res.failed || 0} fehlgeschlagen`,
color: res.failed ? "warning" : "success"
})
await loadRoomMembers()
} catch (error) {
toast.add({
title: "Teilnehmer konnten nicht synchronisiert werden",
color: "error"
})
} finally {
roomMembersSyncing.value = false
}
}
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
await loadRoomMessages({ silent })
@@ -752,15 +779,26 @@ onBeforeUnmount(stopMatrixAutoRefresh)
<h4 class="text-xs font-medium uppercase text-muted">
Teilnehmer
</h4>
<UButton
icon="i-heroicons-arrow-path"
color="neutral"
variant="ghost"
size="xs"
:loading="matrixMembersLoading"
:disabled="!canUseMatrixChat || !activeRoom?.exists"
@click="loadRoomMembers"
/>
<div class="flex items-center gap-1">
<UButton
icon="i-heroicons-user-plus"
color="neutral"
variant="ghost"
size="xs"
:loading="roomMembersSyncing"
:disabled="!canUseMatrixChat || !activeRoom?.exists"
@click="syncRoomMembers"
/>
<UButton
icon="i-heroicons-arrow-path"
color="neutral"
variant="ghost"
size="xs"
:loading="matrixMembersLoading"
:disabled="!canUseMatrixChat || !activeRoom?.exists"
@click="loadRoomMembers"
/>
</div>
</div>
<div class="space-y-2">