KI-AGENT: Matrix-Räume in FEDEO provisionieren
This commit is contained in:
@@ -122,6 +122,20 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) =>
|
const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) =>
|
||||||
`#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}`
|
`#${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 = (
|
const buildSharedSecretMac = (
|
||||||
nonce: string,
|
nonce: string,
|
||||||
username: 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 {
|
return {
|
||||||
getStatus,
|
getStatus,
|
||||||
matrixUserIdForUser,
|
matrixUserIdForUser,
|
||||||
@@ -466,5 +614,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
provisionCurrentUser,
|
provisionCurrentUser,
|
||||||
getTenantSpaceStatus,
|
getTenantSpaceStatus,
|
||||||
provisionCurrentTenantSpace,
|
provisionCurrentTenantSpace,
|
||||||
|
getTenantRoomStatus,
|
||||||
|
provisionTenantRoom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,4 +49,29 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
.send({ error: err.message || "Matrix tenant space provisioning failed" })
|
.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" })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ const { $api } = useNuxtApp()
|
|||||||
const status = ref(null)
|
const status = ref(null)
|
||||||
const identity = ref(null)
|
const identity = ref(null)
|
||||||
const tenantSpace = ref(null)
|
const tenantSpace = ref(null)
|
||||||
|
const generalRoom = ref(null)
|
||||||
const provisionResult = ref(null)
|
const provisionResult = ref(null)
|
||||||
const tenantSpaceProvisionResult = ref(null)
|
const tenantSpaceProvisionResult = ref(null)
|
||||||
|
const generalRoomProvisionResult = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
const tenantSpaceProvisioning = ref(false)
|
const tenantSpaceProvisioning = ref(false)
|
||||||
|
const generalRoomProvisioning = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
|
|
||||||
const statusItems = computed(() => [
|
const statusItems = computed(() => [
|
||||||
@@ -48,15 +51,17 @@ const statusItems = computed(() => [
|
|||||||
const loadMatrixInfo = async () => {
|
const loadMatrixInfo = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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/status"),
|
||||||
$api("/api/communication/matrix/me"),
|
$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
|
status.value = statusRes
|
||||||
identity.value = identityRes
|
identity.value = identityRes
|
||||||
tenantSpace.value = tenantSpaceRes
|
tenantSpace.value = tenantSpaceRes
|
||||||
|
generalRoom.value = generalRoomRes
|
||||||
lastUpdated.value = new Date()
|
lastUpdated.value = new Date()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
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) => {
|
const formatDateTime = (value) => {
|
||||||
if (!value) return "-"
|
if (!value) return "-"
|
||||||
|
|
||||||
@@ -334,6 +377,73 @@ onMounted(loadMatrixInfo)
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">
|
||||||
|
Allgemeiner Chat
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
|
Alias
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
||||||
|
{{ generalRoom?.alias || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
|
Status
|
||||||
|
</p>
|
||||||
|
<UBadge
|
||||||
|
class="mt-1"
|
||||||
|
:color="generalRoom?.exists ? 'success' : 'neutral'"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
{{ generalRoom?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
|
Raum-ID
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
||||||
|
{{ generalRoom?.roomId || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="generalRoomProvisionResult"
|
||||||
|
icon="i-heroicons-check-circle"
|
||||||
|
color="success"
|
||||||
|
variant="soft"
|
||||||
|
:title="generalRoomProvisionResult.alreadyExisted ? 'Allgemeiner Chat vorhanden' : 'Allgemeiner Chat erstellt'"
|
||||||
|
:description="generalRoomProvisionResult.alias"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:loading="generalRoomProvisioning"
|
||||||
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
||||||
|
@click="provisionGeneralRoom"
|
||||||
|
>
|
||||||
|
Allgemeinen Chat erstellen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
|||||||
Reference in New Issue
Block a user