KI-AGENT: Matrix-Räume persistent verwalten
This commit is contained in:
43
backend/db/migrations/0038_communication_rooms.sql
Normal file
43
backend/db/migrations/0038_communication_rooms.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
CREATE TABLE "communication_rooms" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"topic" text,
|
||||
"type" text DEFAULT 'room' NOT NULL,
|
||||
"entity_type" text,
|
||||
"entity_id" bigint,
|
||||
"entity_uuid" uuid,
|
||||
"matrix_room_id" text,
|
||||
"matrix_alias" text,
|
||||
"parent_space_room_id" text,
|
||||
"archived" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"created_by" uuid,
|
||||
"updated_by" uuid
|
||||
);
|
||||
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
|
||||
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||
ON DELETE cascade ON UPDATE no action;
|
||||
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
|
||||
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||
ON DELETE no action ON UPDATE no action;
|
||||
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
|
||||
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||
ON DELETE no action ON UPDATE no action;
|
||||
|
||||
CREATE UNIQUE INDEX "communication_rooms_tenant_key_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id", "key");
|
||||
|
||||
CREATE INDEX "communication_rooms_tenant_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id");
|
||||
|
||||
CREATE INDEX "communication_rooms_entity_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");
|
||||
57
backend/db/schema/communication_rooms.ts
Normal file
57
backend/db/schema/communication_rooms.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
uniqueIndex,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const communicationRooms = pgTable(
|
||||
"communication_rooms",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
key: text("key").notNull(),
|
||||
name: text("name").notNull(),
|
||||
topic: text("topic"),
|
||||
type: text("type").notNull().default("room"),
|
||||
|
||||
entityType: text("entity_type"),
|
||||
entityId: bigint("entity_id", { mode: "number" }),
|
||||
entityUuid: uuid("entity_uuid"),
|
||||
|
||||
matrixRoomId: text("matrix_room_id"),
|
||||
matrixAlias: text("matrix_alias"),
|
||||
parentSpaceRoomId: text("parent_space_room_id"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
},
|
||||
(table) => ({
|
||||
tenantKeyIdx: uniqueIndex("communication_rooms_tenant_key_idx")
|
||||
.on(table.tenantId, table.key),
|
||||
tenantIdx: index("communication_rooms_tenant_idx")
|
||||
.on(table.tenantId),
|
||||
entityIdx: index("communication_rooms_entity_idx")
|
||||
.on(table.tenantId, table.entityType, table.entityId, table.entityUuid),
|
||||
})
|
||||
)
|
||||
|
||||
export type CommunicationRoom = typeof communicationRooms.$inferSelect
|
||||
export type NewCommunicationRoom = typeof communicationRooms.$inferInsert
|
||||
@@ -14,6 +14,7 @@ export * from "./branches"
|
||||
export * from "./checkexecutions"
|
||||
export * from "./checks"
|
||||
export * from "./citys"
|
||||
export * from "./communication_rooms"
|
||||
export * from "./contacts"
|
||||
export * from "./contracts"
|
||||
export * from "./contracttypes"
|
||||
|
||||
@@ -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, tenants } from "../../db/schema"
|
||||
import { authProfiles, authUsers, communicationRooms, tenants } from "../../db/schema"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import { secrets } from "../utils/secrets"
|
||||
|
||||
@@ -39,6 +39,10 @@ type MatrixTenantRoomOptions = {
|
||||
key?: string
|
||||
name?: string
|
||||
topic?: string
|
||||
type?: string
|
||||
entityType?: string | null
|
||||
entityId?: number | null
|
||||
entityUuid?: string | null
|
||||
}
|
||||
|
||||
type MatrixCachedValue<T = any> = {
|
||||
@@ -54,10 +58,11 @@ const matrixTenantSpaceCache = new Map<string, MatrixCachedValue>()
|
||||
const matrixTenantRoomCache = new Map<string, MatrixCachedValue>()
|
||||
let matrixServiceSessionCache: MatrixUserSession | null = null
|
||||
|
||||
const defaultTenantRooms: Required<Pick<MatrixTenantRoomOptions, "key" | "name">>[] = [
|
||||
const defaultTenantRooms: Required<Pick<MatrixTenantRoomOptions, "key" | "name" | "type">>[] = [
|
||||
{
|
||||
key: "allgemein",
|
||||
name: "Allgemeiner Chat",
|
||||
type: "general",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -196,6 +201,10 @@ export function matrixService(server: FastifyInstance) {
|
||||
key,
|
||||
name,
|
||||
topic,
|
||||
type: options.type || "room",
|
||||
entityType: options.entityType || null,
|
||||
entityId: options.entityId || null,
|
||||
entityUuid: options.entityUuid || null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,28 +627,154 @@ export function matrixService(server: FastifyInstance) {
|
||||
return value
|
||||
}
|
||||
|
||||
const roomMetadataToApi = (
|
||||
tenant: { id: number, name?: string | null, short?: string | null },
|
||||
room: typeof communicationRooms.$inferSelect
|
||||
) => ({
|
||||
id: room.id,
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
key: room.key,
|
||||
name: room.name,
|
||||
topic: room.topic,
|
||||
type: room.type,
|
||||
entityType: room.entityType,
|
||||
entityId: room.entityId,
|
||||
entityUuid: room.entityUuid,
|
||||
alias: room.matrixAlias || tenantRoomAlias(tenant, room.key),
|
||||
exists: Boolean(room.matrixRoomId),
|
||||
roomId: room.matrixRoomId,
|
||||
parentSpaceRoomId: room.parentSpaceRoomId,
|
||||
servers: [],
|
||||
archived: room.archived,
|
||||
})
|
||||
|
||||
const findTenantRoomMetadata = async (tenantId: number, key: string) => {
|
||||
const [room] = await server.db
|
||||
.select()
|
||||
.from(communicationRooms)
|
||||
.where(and(
|
||||
eq(communicationRooms.tenantId, tenantId),
|
||||
eq(communicationRooms.key, key)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
const ensureTenantRoomMetadata = async (
|
||||
tenant: { id: number, name?: string | null, short?: string | null },
|
||||
options: MatrixTenantRoomOptions
|
||||
) => {
|
||||
const normalizedOptions = normalizeTenantRoomOptions(options)
|
||||
const existing = await findTenantRoomMetadata(tenant.id, normalizedOptions.key)
|
||||
|
||||
if (existing) {
|
||||
const shouldUpdate =
|
||||
(options.name !== undefined && existing.name !== normalizedOptions.name) ||
|
||||
(options.topic !== undefined && existing.topic !== normalizedOptions.topic) ||
|
||||
(options.type !== undefined && existing.type !== normalizedOptions.type) ||
|
||||
(options.entityType !== undefined && existing.entityType !== normalizedOptions.entityType) ||
|
||||
(options.entityId !== undefined && existing.entityId !== normalizedOptions.entityId) ||
|
||||
(options.entityUuid !== undefined && existing.entityUuid !== normalizedOptions.entityUuid)
|
||||
|
||||
if (!shouldUpdate) return existing
|
||||
|
||||
const [updated] = await server.db
|
||||
.update(communicationRooms)
|
||||
.set({
|
||||
name: options.name !== undefined ? normalizedOptions.name : existing.name,
|
||||
topic: options.topic !== undefined ? normalizedOptions.topic : existing.topic,
|
||||
type: options.type !== undefined ? normalizedOptions.type : existing.type,
|
||||
entityType: options.entityType !== undefined ? normalizedOptions.entityType : existing.entityType,
|
||||
entityId: options.entityId !== undefined ? normalizedOptions.entityId : existing.entityId,
|
||||
entityUuid: options.entityUuid !== undefined ? normalizedOptions.entityUuid : existing.entityUuid,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(communicationRooms.id, existing.id))
|
||||
.returning()
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
const [created] = await server.db
|
||||
.insert(communicationRooms)
|
||||
.values({
|
||||
tenantId: tenant.id,
|
||||
key: normalizedOptions.key,
|
||||
name: normalizedOptions.name,
|
||||
topic: normalizedOptions.topic,
|
||||
type: normalizedOptions.type,
|
||||
entityType: normalizedOptions.entityType,
|
||||
entityId: normalizedOptions.entityId,
|
||||
entityUuid: normalizedOptions.entityUuid,
|
||||
matrixAlias: tenantRoomAlias(tenant, normalizedOptions.key),
|
||||
})
|
||||
.returning()
|
||||
|
||||
return created
|
||||
}
|
||||
|
||||
const markTenantRoomProvisioned = async (
|
||||
metadataId: string,
|
||||
values: {
|
||||
matrixRoomId: string
|
||||
matrixAlias: string
|
||||
parentSpaceRoomId?: string | null
|
||||
}
|
||||
) => {
|
||||
const [updated] = await server.db
|
||||
.update(communicationRooms)
|
||||
.set({
|
||||
matrixRoomId: values.matrixRoomId,
|
||||
matrixAlias: values.matrixAlias,
|
||||
parentSpaceRoomId: values.parentSpaceRoomId || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(communicationRooms.id, metadataId))
|
||||
.returning()
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
const getTenantRoomStatus = async (
|
||||
tenantId: number | null,
|
||||
roomKey: string,
|
||||
roomName: string
|
||||
roomName?: string
|
||||
) => {
|
||||
const tenant = await getCurrentTenant(tenantId)
|
||||
const alias = tenantRoomAlias(tenant, roomKey)
|
||||
const normalizedOptions = normalizeTenantRoomOptions({ key: roomKey, name: roomName })
|
||||
const metadata = await ensureTenantRoomMetadata(tenant, normalizedOptions)
|
||||
const alias = metadata.matrixAlias || tenantRoomAlias(tenant, normalizedOptions.key)
|
||||
|
||||
try {
|
||||
const directoryEntry = await requestJson<{
|
||||
room_id: string
|
||||
servers: string[]
|
||||
}>(`${homeserverUrl()}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`)
|
||||
const roomMetadata = metadata.matrixRoomId === directoryEntry.room_id
|
||||
? metadata
|
||||
: await markTenantRoomProvisioned(metadata.id, {
|
||||
matrixRoomId: directoryEntry.room_id,
|
||||
matrixAlias: alias,
|
||||
parentSpaceRoomId: metadata.parentSpaceRoomId,
|
||||
})
|
||||
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
key: roomKey,
|
||||
name: roomName,
|
||||
id: roomMetadata.id,
|
||||
key: roomMetadata.key,
|
||||
name: roomMetadata.name,
|
||||
topic: roomMetadata.topic,
|
||||
type: roomMetadata.type,
|
||||
entityType: roomMetadata.entityType,
|
||||
entityId: roomMetadata.entityId,
|
||||
entityUuid: roomMetadata.entityUuid,
|
||||
alias,
|
||||
exists: true,
|
||||
roomId: directoryEntry.room_id,
|
||||
parentSpaceRoomId: roomMetadata.parentSpaceRoomId,
|
||||
servers: directoryEntry.servers,
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -647,11 +782,18 @@ export function matrixService(server: FastifyInstance) {
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
key: roomKey,
|
||||
name: roomName,
|
||||
id: metadata.id,
|
||||
key: metadata.key,
|
||||
name: metadata.name,
|
||||
topic: metadata.topic,
|
||||
type: metadata.type,
|
||||
entityType: metadata.entityType,
|
||||
entityId: metadata.entityId,
|
||||
entityUuid: metadata.entityUuid,
|
||||
alias,
|
||||
exists: false,
|
||||
roomId: null,
|
||||
roomId: metadata.matrixRoomId,
|
||||
parentSpaceRoomId: metadata.parentSpaceRoomId,
|
||||
servers: [],
|
||||
}
|
||||
}
|
||||
@@ -667,6 +809,7 @@ export function matrixService(server: FastifyInstance) {
|
||||
) => {
|
||||
const tenant = await getCurrentTenant(tenantId)
|
||||
const normalizedOptions = normalizeTenantRoomOptions(options)
|
||||
const metadata = await ensureTenantRoomMetadata(tenant, normalizedOptions)
|
||||
const key = normalizedOptions.key
|
||||
const name = normalizedOptions.name
|
||||
const topic = (normalizedOptions.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim()
|
||||
@@ -682,6 +825,12 @@ export function matrixService(server: FastifyInstance) {
|
||||
const tenantSpace = await provisionCurrentTenantSpace(userId, tenant.id)
|
||||
|
||||
if (existing.exists) {
|
||||
await markTenantRoomProvisioned(metadata.id, {
|
||||
matrixRoomId: existing.roomId,
|
||||
matrixAlias: existing.alias,
|
||||
parentSpaceRoomId: existing.parentSpaceRoomId,
|
||||
})
|
||||
|
||||
const value = {
|
||||
...existing,
|
||||
created: false,
|
||||
@@ -748,10 +897,16 @@ export function matrixService(server: FastifyInstance) {
|
||||
)
|
||||
|
||||
const value = {
|
||||
id: metadata.id,
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
key,
|
||||
name,
|
||||
topic,
|
||||
type: metadata.type,
|
||||
entityType: metadata.entityType,
|
||||
entityId: metadata.entityId,
|
||||
entityUuid: metadata.entityUuid,
|
||||
alias: tenantRoomAlias(tenant, key),
|
||||
exists: true,
|
||||
created: true,
|
||||
@@ -762,6 +917,12 @@ export function matrixService(server: FastifyInstance) {
|
||||
serviceUserId: serviceLogin.matrixUserId,
|
||||
}
|
||||
|
||||
await markTenantRoomProvisioned(metadata.id, {
|
||||
matrixRoomId: value.roomId,
|
||||
matrixAlias: value.alias,
|
||||
parentSpaceRoomId: value.parentSpaceRoomId,
|
||||
})
|
||||
|
||||
matrixTenantRoomCache.set(cacheKey, {
|
||||
exists: true,
|
||||
cachedUntil: Date.now() + 30 * 60 * 1000,
|
||||
@@ -805,26 +966,23 @@ export function matrixService(server: FastifyInstance) {
|
||||
|
||||
const listTenantRooms = async (tenantId: number | null) => {
|
||||
const tenant = await getCurrentTenant(tenantId)
|
||||
const rooms = new Map<string, any>()
|
||||
|
||||
for (const room of defaultTenantRooms) {
|
||||
rooms.set(room.key, await getTenantRoomStatus(tenant.id, room.key, room.name))
|
||||
await ensureTenantRoomMetadata(tenant, room)
|
||||
}
|
||||
|
||||
for (const [cacheKey, cachedRoom] of matrixTenantRoomCache.entries()) {
|
||||
const [cachedTenantId, roomKey] = cacheKey.split(":")
|
||||
if (cachedTenantId !== String(tenant.id) || !cachedRoom.value) continue
|
||||
|
||||
rooms.set(roomKey, {
|
||||
...cachedRoom.value,
|
||||
exists: true,
|
||||
})
|
||||
}
|
||||
const rooms = await server.db
|
||||
.select()
|
||||
.from(communicationRooms)
|
||||
.where(and(
|
||||
eq(communicationRooms.tenantId, tenant.id),
|
||||
eq(communicationRooms.archived, false)
|
||||
))
|
||||
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
rooms: Array.from(rooms.values()).sort((a, b) =>
|
||||
rooms: rooms.map((room) => roomMetadataToApi(tenant, room)).sort((a, b) =>
|
||||
String(a.name || a.key).localeCompare(String(b.name || b.key), "de")
|
||||
),
|
||||
}
|
||||
|
||||
@@ -12,12 +12,24 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
|
||||
const roomOptionsFromRequest = (req: any) => {
|
||||
const params = req.params as { roomKey?: string }
|
||||
const body = (req.body || {}) as { key?: string, name?: string, topic?: string }
|
||||
const body = (req.body || {}) as {
|
||||
key?: string
|
||||
name?: string
|
||||
topic?: string
|
||||
type?: string
|
||||
entityType?: string | null
|
||||
entityId?: number | null
|
||||
entityUuid?: string | null
|
||||
}
|
||||
|
||||
return {
|
||||
key: params.roomKey || body.key,
|
||||
name: body.name,
|
||||
topic: body.topic,
|
||||
type: body.type,
|
||||
entityType: body.entityType,
|
||||
entityId: body.entityId,
|
||||
entityUuid: body.entityUuid,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +137,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
server.get("/communication/matrix/rooms/:roomKey", async (req, reply) => {
|
||||
try {
|
||||
const params = req.params as { roomKey: string }
|
||||
return await matrix.getTenantRoomStatus(req.user.tenant_id, params.roomKey, params.roomKey)
|
||||
return await matrix.getTenantRoomStatus(req.user.tenant_id, params.roomKey)
|
||||
} catch (err: any) {
|
||||
return handleMatrixError(req, reply, err, "Matrix room status failed")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user