KI-AGENT: Matrix-Teilnehmer synchronisieren
This commit is contained in:
@@ -2,7 +2,7 @@ import { createHash, createHmac, randomBytes } from "node:crypto"
|
|||||||
import { existsSync, readFileSync } from "node:fs"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
import { resolve } from "node:path"
|
import { resolve } from "node:path"
|
||||||
import { FastifyInstance } from "fastify"
|
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 { and, eq } from "drizzle-orm"
|
||||||
import { secrets } from "../utils/secrets"
|
import { secrets } from "../utils/secrets"
|
||||||
|
|
||||||
@@ -944,21 +944,15 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await requestMatrixJson(
|
||||||
await requestMatrixJson(
|
`/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`,
|
||||||
`/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`,
|
session.accessToken,
|
||||||
session.accessToken,
|
{
|
||||||
{
|
method: "POST",
|
||||||
method: "POST",
|
headers: { "Content-Type": "application/json" },
|
||||||
headers: { "Content-Type": "application/json" },
|
body: JSON.stringify({}),
|
||||||
body: JSON.stringify({}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.errcode !== "M_FORBIDDEN" && err.errcode !== "M_UNKNOWN") {
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
matrixJoinedRoomCache.set(joinCacheKey, Date.now() + 30 * 60 * 1000)
|
matrixJoinedRoomCache.set(joinCacheKey, Date.now() + 30 * 60 * 1000)
|
||||||
return session
|
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) =>
|
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
|
||||||
getTenantRoomMessages(userId, tenantId, {
|
getTenantRoomMessages(userId, tenantId, {
|
||||||
key: "allgemein",
|
key: "allgemein",
|
||||||
@@ -1149,6 +1262,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
getTenantRoomMessages,
|
getTenantRoomMessages,
|
||||||
getTenantRoomMembers,
|
getTenantRoomMembers,
|
||||||
sendTenantRoomMessage,
|
sendTenantRoomMessage,
|
||||||
|
syncTenantRoomMembers,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
getGeneralRoomMembers,
|
getGeneralRoomMembers,
|
||||||
sendGeneralRoomMessage,
|
sendGeneralRoomMessage,
|
||||||
|
|||||||
@@ -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) => {
|
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
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) => {
|
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const roomCreateForm = ref({
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const roomProvisioning = ref(false)
|
const roomProvisioning = ref(false)
|
||||||
const roomCreating = ref(false)
|
const roomCreating = ref(false)
|
||||||
|
const roomMembersSyncing = ref(false)
|
||||||
const matrixMessagesLoading = ref(false)
|
const matrixMessagesLoading = ref(false)
|
||||||
const matrixMembersLoading = ref(false)
|
const matrixMembersLoading = ref(false)
|
||||||
const matrixMessageSending = 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 } = {}) => {
|
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
|
||||||
await loadRoomMessages({ silent })
|
await loadRoomMessages({ silent })
|
||||||
|
|
||||||
@@ -752,15 +779,26 @@ onBeforeUnmount(stopMatrixAutoRefresh)
|
|||||||
<h4 class="text-xs font-medium uppercase text-muted">
|
<h4 class="text-xs font-medium uppercase text-muted">
|
||||||
Teilnehmer
|
Teilnehmer
|
||||||
</h4>
|
</h4>
|
||||||
<UButton
|
<div class="flex items-center gap-1">
|
||||||
icon="i-heroicons-arrow-path"
|
<UButton
|
||||||
color="neutral"
|
icon="i-heroicons-user-plus"
|
||||||
variant="ghost"
|
color="neutral"
|
||||||
size="xs"
|
variant="ghost"
|
||||||
:loading="matrixMembersLoading"
|
size="xs"
|
||||||
:disabled="!canUseMatrixChat || !activeRoom?.exists"
|
:loading="roomMembersSyncing"
|
||||||
@click="loadRoomMembers"
|
: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>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user