KI-AGENT: Matrix durch nativen FEDEO-Chat ersetzen

This commit is contained in:
2026-09-08 22:42:40 +02:00
parent 47bd8e80e4
commit 358e40d749
47 changed files with 1027 additions and 9766 deletions

View File

@@ -8,9 +8,6 @@ CREATE TABLE "communication_rooms" (
"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,

View File

@@ -8,9 +8,6 @@ CREATE TABLE IF NOT EXISTS "communication_rooms" (
"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,

View File

@@ -0,0 +1,42 @@
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "matrix_room_id";
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "matrix_alias";
ALTER TABLE "communication_rooms" DROP COLUMN IF EXISTS "parent_space_room_id";
CREATE TABLE IF NOT EXISTS "communication_room_members" (
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
"user_id" uuid NOT NULL REFERENCES "auth_users"("id") ON DELETE cascade,
"joined_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "communication_room_members_room_id_user_id_pk" PRIMARY KEY ("room_id", "user_id")
);
CREATE INDEX IF NOT EXISTS "communication_room_members_user_idx"
ON "communication_room_members" ("user_id");
CREATE TABLE IF NOT EXISTS "communication_messages" (
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"tenant_id" bigint NOT NULL REFERENCES "tenants"("id") ON DELETE cascade,
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
"author_user_id" uuid NOT NULL REFERENCES "auth_users"("id"),
"body" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE INDEX IF NOT EXISTS "communication_messages_room_message_idx"
ON "communication_messages" ("room_id", "id");
CREATE INDEX IF NOT EXISTS "communication_messages_tenant_idx"
ON "communication_messages" ("tenant_id");
CREATE TABLE IF NOT EXISTS "communication_room_reads" (
"room_id" uuid NOT NULL REFERENCES "communication_rooms"("id") ON DELETE cascade,
"user_id" uuid NOT NULL REFERENCES "auth_users"("id") ON DELETE cascade,
"last_read_message_id" bigint,
"read_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "communication_room_reads_room_id_user_id_pk" PRIMARY KEY ("room_id", "user_id")
);
INSERT INTO "communication_room_members" ("room_id", "user_id")
SELECT room.id, tenant_user.user_id
FROM "communication_rooms" room
JOIN "auth_tenant_users" tenant_user ON tenant_user.tenant_id = room.tenant_id
WHERE room.type IN ('general', 'room')
ON CONFLICT DO NOTHING;

View File

@@ -470,6 +470,13 @@
"when": 1788803000000,
"tag": "0069_reset_email_entity_suggestions",
"breakpoints": true
},
{
"idx": 67,
"version": "7",
"when": 1788850800000,
"tag": "0070_native_communication_chat",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,34 @@
import { bigint, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import { communicationRooms } from "./communication_rooms"
import { tenants } from "./tenants"
export const communicationMessages = pgTable(
"communication_messages",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
roomId: uuid("room_id")
.notNull()
.references(() => communicationRooms.id, { onDelete: "cascade" }),
authorUserId: uuid("author_user_id")
.notNull()
.references(() => authUsers.id),
body: text("body").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
roomMessageIdx: index("communication_messages_room_message_idx").on(table.roomId, table.id),
tenantIdx: index("communication_messages_tenant_idx").on(table.tenantId),
})
)
export type CommunicationMessage = typeof communicationMessages.$inferSelect
export type NewCommunicationMessage = typeof communicationMessages.$inferInsert

View File

@@ -0,0 +1,26 @@
import { index, pgTable, primaryKey, timestamp, uuid } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import { communicationRooms } from "./communication_rooms"
export const communicationRoomMembers = pgTable(
"communication_room_members",
{
roomId: uuid("room_id")
.notNull()
.references(() => communicationRooms.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade" }),
joinedAt: timestamp("joined_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
pk: primaryKey({ columns: [table.roomId, table.userId] }),
userIdx: index("communication_room_members_user_idx").on(table.userId),
})
)
export type CommunicationRoomMember = typeof communicationRoomMembers.$inferSelect
export type NewCommunicationRoomMember = typeof communicationRoomMembers.$inferInsert

View File

@@ -0,0 +1,26 @@
import { bigint, pgTable, primaryKey, timestamp, uuid } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import { communicationRooms } from "./communication_rooms"
export const communicationRoomReads = pgTable(
"communication_room_reads",
{
roomId: uuid("room_id")
.notNull()
.references(() => communicationRooms.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade" }),
lastReadMessageId: bigint("last_read_message_id", { mode: "number" }),
readAt: timestamp("read_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
pk: primaryKey({ columns: [table.roomId, table.userId] }),
})
)
export type CommunicationRoomRead = typeof communicationRoomReads.$inferSelect
export type NewCommunicationRoomRead = typeof communicationRoomReads.$inferInsert

View File

@@ -30,10 +30,6 @@ export const communicationRooms = pgTable(
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 })

View File

@@ -16,6 +16,9 @@ export * from "./checkexecutions"
export * from "./checks"
export * from "./citys"
export * from "./communication_rooms"
export * from "./communication_room_members"
export * from "./communication_messages"
export * from "./communication_room_reads"
export * from "./contacts"
export * from "./contracts"
export * from "./contracttypes"

View File

@@ -61,7 +61,6 @@ import {loadSecrets, secrets} from "./utils/secrets";
import {initMailer} from "./utils/mailer"
import {initS3} from "./utils/s3";
import { runBootstrap } from "./modules/bootstrap.service";
import { startMatrixPushWorker } from "./modules/matrix-push-worker.service";
import { startCentralServicesHeartbeat } from "./modules/central-services-heartbeat.service";
import { startDocumentImportWorker } from "./modules/document-import/document-import.worker";
@@ -89,7 +88,6 @@ async function main() {
await app.register(dbPlugin);
await app.register(servicesPlugin);
await runBootstrap(app);
startMatrixPushWorker(app);
startCentralServicesHeartbeat(app);
startDocumentImportWorker(app);

View File

@@ -19,7 +19,6 @@ import {
tenants,
texttemplates,
} from "../../db/schema"
import { matrixService } from "./matrix.service"
const adminPermissions = [
"mcp.tokens.write",
@@ -456,18 +455,4 @@ export async function runBootstrap(server: FastifyInstance) {
await ensureTenantBaseData(server, tenant.id, adminUser.id)
console.log("✅ Bootstrap-Grunddaten geprüft")
if (process.env.FEDEO_BOOTSTRAP_MATRIX === "true") {
try {
const matrix = matrixService(server)
await matrix.provisionTenantRoom(adminUser.id, tenant.id, {
key: "allgemein",
name: "Allgemeiner Chat",
type: "general",
})
console.log("✅ Bootstrap-Matrix-Kommunikation geprüft")
} catch (err) {
console.error("❌ Bootstrap-Matrix-Kommunikation fehlgeschlagen:", err)
throw err
}
}
}

View File

@@ -1,377 +0,0 @@
import { createHash } from "node:crypto"
import type { FastifyInstance } from "fastify"
import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm"
import { authProfiles, authTenantUsers, authUsers, communicationRooms, notificationsItems } from "../../db/schema"
import { matrixService } from "./matrix.service"
import { NotificationService, UserDirectory } from "./notification.service"
type ChatRecipient = {
userId: string
email?: string | null
firstName?: string | null
lastName?: string | null
fullName?: string | null
matrixUserId?: string
}
type MatrixPushWorkerEvent = {
at: string
type: string
roomKey?: string
roomId?: string | null
messageId?: string
sender?: string
targets?: number
created?: number
delivered?: number
failed?: number
error?: string
}
const matrixPushWorkerState = {
enabled: false,
startedAt: null as string | null,
lastRunAt: null as string | null,
lastJoinAt: null as string | null,
lastJoinTotal: 0,
lastJoinJoined: 0,
lastJoinFailed: 0,
hasSyncToken: false,
lastSyncRooms: 0,
lastSyncMessages: 0,
lastMatchedRooms: 0,
lastNotificationsCreated: 0,
lastNotificationsDelivered: 0,
lastNotificationsFailed: 0,
lastError: null as string | null,
events: [] as MatrixPushWorkerEvent[],
}
const rememberWorkerEvent = (event: MatrixPushWorkerEvent) => {
matrixPushWorkerState.events = [
{
at: new Date().toISOString(),
...event,
},
...matrixPushWorkerState.events,
].slice(0, 25)
}
export const getMatrixPushWorkerState = () => ({
...matrixPushWorkerState,
events: [...matrixPushWorkerState.events],
})
const getUserDirectory: UserDirectory = async (server: FastifyInstance, userId) => {
const rows = await server.db
.select({ email: authUsers.email })
.from(authUsers)
.where(eq(authUsers.id, userId))
.limit(1)
return rows[0] || null
}
const displayUserName = (user: { fullName?: string | null; firstName?: string | null; lastName?: string | null; email?: string | null }) => {
const name = user.fullName || [user.firstName, user.lastName].filter(Boolean).join(" ")
return name || user.email || "Benutzer"
}
const directRoomKey = (firstUserId: string, secondUserId: string) => {
const hash = createHash("sha256")
.update([firstUserId, secondUserId].sort().join(":"))
.digest("hex")
.slice(0, 16)
return `direct_${hash}`
}
const mentionAliasesForUser = (user: ChatRecipient) => {
const name = displayUserName(user)
return Array.from(new Set([
name,
user.fullName,
[user.firstName, user.lastName].filter(Boolean).join(" "),
user.firstName,
user.email,
].filter(Boolean).map((value) => String(value).toLowerCase())))
}
const mentionedRecipientIds = (text: string, recipients: ChatRecipient[]) => {
const normalizedText = text.toLowerCase()
return recipients
.filter((recipient) => mentionAliasesForUser(recipient).some((alias) =>
normalizedText.includes(`@${alias}`)
))
.map((recipient) => recipient.userId)
}
export function startMatrixPushWorker(server: FastifyInstance) {
if (process.env.MATRIX_PUSH_WORKER_DISABLED === "1") {
server.log.info("Matrix-Push-Worker ist deaktiviert")
return
}
matrixPushWorkerState.enabled = true
matrixPushWorkerState.startedAt = new Date().toISOString()
rememberWorkerEvent({ at: new Date().toISOString(), type: "started" })
const matrix = matrixService(server)
const notifications = new NotificationService(server, getUserDirectory)
const intervalMs = Math.max(Number(process.env.MATRIX_PUSH_WORKER_INTERVAL_MS || 3000), 1000)
let since: string | undefined
let running = false
let stopped = false
let timer: ReturnType<typeof setTimeout> | undefined
let lastServiceJoinSyncAt = 0
let errorBackoffMs = 0
const getTenantRecipients = async (tenantId: number) => {
const rows = await server.db
.select({
userId: authTenantUsers.user_id,
email: authUsers.email,
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, tenantId)
))
.where(eq(authTenantUsers.tenant_id, tenantId))
return await Promise.all(rows.map(async (row) => ({
...row,
matrixUserId: await matrix.matrixUserIdForUser(row.userId, tenantId),
})))
}
const hasChatNotificationForMessage = async (tenantId: number, userId: string, messageId: string) => {
const rows = await server.db
.select({
payload: notificationsItems.payload,
})
.from(notificationsItems)
.where(and(
eq(notificationsItems.tenantId, tenantId),
eq(notificationsItems.userId, userId),
eq(notificationsItems.eventType, "communication.message.new")
))
.orderBy(desc(notificationsItems.createdAt))
.limit(200)
return rows.some((row) => (row.payload as any)?.messageId === messageId)
}
const recipientsForMessage = (
room: typeof communicationRooms.$inferSelect,
recipients: ChatRecipient[],
senderUserId: string | null,
text: string
) => {
const candidates = senderUserId
? recipients.filter((recipient) => recipient.userId !== senderUserId)
: recipients
const mentioned = new Set(mentionedRecipientIds(text, candidates))
const directRecipients = new Set<string>()
if (room.type === "direct" && room.entityUuid && room.entityUuid !== senderUserId) {
directRecipients.add(room.entityUuid)
} else if (room.type === "direct" && senderUserId) {
candidates
.filter((recipient) => directRoomKey(senderUserId, recipient.userId) === room.key)
.forEach((recipient) => directRecipients.add(recipient.userId))
}
return candidates
.filter((recipient) => directRecipients.has(recipient.userId) || mentioned.has(recipient.userId))
.map((recipient) => ({
...recipient,
mentioned: mentioned.has(recipient.userId),
direct: directRecipients.has(recipient.userId),
}))
}
const deliverMessageNotification = async (
room: typeof communicationRooms.$inferSelect,
message: any,
recipients: ChatRecipient[]
) => {
if (!message.id || message.own) return
const sender = recipients.find((recipient) => recipient.matrixUserId === message.sender) || null
const text = message.body || message.attachment?.fileName || "Neue Nachricht"
const targets = recipientsForMessage(room, recipients, sender?.userId || null, text)
rememberWorkerEvent({
at: new Date().toISOString(),
type: "message_seen",
roomKey: room.key,
roomId: room.matrixRoomId,
messageId: message.id,
sender: message.sender,
targets: targets.length,
})
if (!targets.length) return
const senderName = sender ? displayUserName(sender) : message.senderDisplayName || message.sender || "Matrix"
const preview = text.length > 160 ? `${text.slice(0, 157)}...` : text
for (const target of targets) {
if (await hasChatNotificationForMessage(room.tenantId, target.userId, message.id)) {
rememberWorkerEvent({
at: new Date().toISOString(),
type: "notification_skipped_duplicate",
roomKey: room.key,
roomId: room.matrixRoomId,
messageId: message.id,
sender: message.sender,
targets: 1,
})
continue
}
const result = await notifications.trigger({
tenantId: room.tenantId,
userId: target.userId,
eventType: "communication.message.new",
title: target.mentioned ? `${senderName} hat dich erwähnt` : `Neue Direktnachricht von ${senderName}`,
message: preview,
payload: {
link: `/communication/chat?room=${encodeURIComponent(room.key)}`,
roomKey: room.key,
roomName: room.name,
roomType: room.type,
messageId: message.id,
matrixSender: message.sender,
mentioned: target.mentioned,
direct: target.direct,
},
channels: ["inapp", "push"],
})
matrixPushWorkerState.lastNotificationsCreated += result.created || 0
matrixPushWorkerState.lastNotificationsDelivered += result.delivered || 0
matrixPushWorkerState.lastNotificationsFailed += result.failed || 0
rememberWorkerEvent({
at: new Date().toISOString(),
type: "notification_triggered",
roomKey: room.key,
roomId: room.matrixRoomId,
messageId: message.id,
sender: message.sender,
targets: 1,
created: result.created || 0,
delivered: result.delivered || 0,
failed: result.failed || 0,
})
}
}
const runOnce = async () => {
if (running || stopped) return
running = true
try {
matrixPushWorkerState.lastRunAt = new Date().toISOString()
matrixPushWorkerState.lastError = null
matrixPushWorkerState.lastSyncRooms = 0
matrixPushWorkerState.lastSyncMessages = 0
matrixPushWorkerState.lastMatchedRooms = 0
matrixPushWorkerState.lastNotificationsCreated = 0
matrixPushWorkerState.lastNotificationsDelivered = 0
matrixPushWorkerState.lastNotificationsFailed = 0
if (!lastServiceJoinSyncAt || Date.now() - lastServiceJoinSyncAt > 60_000) {
const joinResult = await matrix.syncServiceJoinedTenantRooms()
lastServiceJoinSyncAt = Date.now()
matrixPushWorkerState.lastJoinAt = new Date().toISOString()
matrixPushWorkerState.lastJoinTotal = joinResult.total
matrixPushWorkerState.lastJoinJoined = joinResult.joined
matrixPushWorkerState.lastJoinFailed = joinResult.failed
rememberWorkerEvent({
at: new Date().toISOString(),
type: "service_join_sync",
targets: joinResult.total,
delivered: joinResult.joined,
failed: joinResult.failed,
})
if (joinResult.failed) {
console.warn("Matrix-Push-Worker: Service-User konnte nicht alle Räume joinen", {
total: joinResult.total,
joined: joinResult.joined,
failed: joinResult.failed,
})
}
}
const initial = !since
const sync = await matrix.syncServiceRoomEvents(since, initial)
since = sync.nextBatch || since
matrixPushWorkerState.hasSyncToken = Boolean(since)
matrixPushWorkerState.lastSyncRooms = sync.rooms?.length || 0
matrixPushWorkerState.lastSyncMessages = (sync.rooms || [])
.reduce((sum: number, room: any) => sum + (room.messages?.length || 0), 0)
if (!initial && sync.rooms?.length) {
const roomIds = sync.rooms.map((room: any) => room.roomId).filter(Boolean)
const rooms = roomIds.length
? await server.db
.select()
.from(communicationRooms)
.where(and(
inArray(communicationRooms.matrixRoomId, roomIds),
ne(communicationRooms.archived, true),
isNotNull(communicationRooms.matrixRoomId)
))
: []
const roomsByMatrixId = new Map(rooms.map((room) => [room.matrixRoomId, room]))
matrixPushWorkerState.lastMatchedRooms = rooms.length
const recipientsByTenant = new Map<number, ChatRecipient[]>()
for (const syncedRoom of sync.rooms) {
const room = roomsByMatrixId.get(syncedRoom.roomId)
if (!room || !syncedRoom.messages?.length) continue
if (!recipientsByTenant.has(room.tenantId)) {
recipientsByTenant.set(room.tenantId, await getTenantRecipients(room.tenantId))
}
const recipients = recipientsByTenant.get(room.tenantId) || []
for (const message of syncedRoom.messages) {
await deliverMessageNotification(room, message, recipients)
}
}
}
errorBackoffMs = 0
} catch (err) {
matrixPushWorkerState.lastError = err instanceof Error ? err.message : String(err)
const retryAfterMs = Number((err as any)?.retryAfterMs || (err as any)?.body?.retry_after_ms || 0)
errorBackoffMs = Math.min(
Math.max(retryAfterMs || (errorBackoffMs ? errorBackoffMs * 2 : 30_000), 30_000),
5 * 60_000
)
rememberWorkerEvent({
at: new Date().toISOString(),
type: "error",
error: matrixPushWorkerState.lastError,
})
console.error("Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten", err)
server.log.error({ err }, "Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten")
} finally {
running = false
if (!stopped) {
const nextDelay = errorBackoffMs || (since ? 0 : intervalMs)
timer = setTimeout(() => void runOnce(), nextDelay)
}
}
}
timer = setTimeout(() => void runOnce(), intervalMs)
server.addHook("onClose", async () => {
stopped = true
if (timer) clearTimeout(timer)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import { FastifyInstance } from "fastify"
import { matrixService } from "./matrix.service"
type MetricSample = {
labels: Record<string, string>
@@ -117,10 +116,6 @@ export const buildSystemStatus = async (server: FastifyInstance) => {
const uname = nodeMetrics?.get("node_uname_info")?.[0]?.labels || null
const databaseCheck = await server.db.execute("SELECT NOW() as now")
const matrixStatus = await matrixService(server).getStatus().catch((err: any) => ({
reachable: false,
error: err?.message || "Matrix-Status nicht verfügbar",
}))
const minioUrl = s3EndpointUrl()
return {
@@ -165,7 +160,6 @@ export const buildSystemStatus = async (server: FastifyInstance) => {
url: nodeExporterMetricsUrl,
error: nodeExporterError,
}),
matrix: serviceState(Boolean((matrixStatus as any).reachable), matrixStatus as Record<string, any>),
minio: minioUrl ? await checkHttp(`${minioUrl}/minio/health/live`) : serviceState(false, {
error: "S3_ENDPOINT ist nicht gesetzt",
}),

View File

@@ -30,7 +30,6 @@ import {
} from "../utils/tenantFullExport";
import type { TenantFullExport } from "../utils/tenantFullExport";
import { buildSystemStatus } from "../modules/system-status.service";
import { matrixService } from "../modules/matrix.service";
import { s3 } from "../utils/s3";
import { secrets } from "../utils/secrets";
@@ -385,27 +384,7 @@ export default async function adminRoutes(server: FastifyInstance) {
});
}
let matrixProvisioned = false;
let matrixProvisioningError: string | null = null;
if (process.env.MATRIX_REGISTRATION_SHARED_SECRET) {
try {
const matrix = matrixService(server);
await matrix.provisionTenantRoom(currentUser.id, result.tenantId, {
key: "allgemein",
name: "Allgemeiner Chat",
type: "general",
});
matrixProvisioned = true;
} catch (err: any) {
matrixProvisioningError = err?.message || String(err);
server.log.warn({ err }, "Matrix-Räume konnten nach Tenant-Import nicht neu provisioniert werden");
}
}
return {
matrixProvisioned,
matrixProvisioningError,
};
return { chatReady: true };
};
const startTenantExportJob = async (jobId: string, tenantId: number, filename: string) => {

File diff suppressed because it is too large Load Diff

View File

@@ -38,15 +38,6 @@ export let secrets = {
DOKUBOX_IMAP_PASSWORD: string
OPENAI_API_KEY: string
STIRLING_API_KEY: string
MATRIX_HOMESERVER_URL?: string
MATRIX_SERVER_NAME?: string
MATRIX_RTC_HOST?: string
MATRIX_RTC_JWT_URL?: string
MATRIX_LIVEKIT_URL?: string
MATRIX_REGISTRATION_SHARED_SECRET?: string
MATRIX_SERVICE_USER_LOCALPART?: string
LIVEKIT_KEY?: string
LIVEKIT_SECRET?: string
WEB_PUSH_PUBLIC_KEY?: string
WEB_PUSH_PRIVATE_KEY?: string
WEB_PUSH_SUBJECT?: string
@@ -88,15 +79,6 @@ const secretKeys = [
"DOKUBOX_IMAP_PASSWORD",
"OPENAI_API_KEY",
"STIRLING_API_KEY",
"MATRIX_HOMESERVER_URL",
"MATRIX_SERVER_NAME",
"MATRIX_RTC_HOST",
"MATRIX_RTC_JWT_URL",
"MATRIX_LIVEKIT_URL",
"MATRIX_REGISTRATION_SHARED_SECRET",
"MATRIX_SERVICE_USER_LOCALPART",
"LIVEKIT_KEY",
"LIVEKIT_SECRET",
"WEB_PUSH_PUBLIC_KEY",
"WEB_PUSH_PRIVATE_KEY",
"WEB_PUSH_SUBJECT",

View File

@@ -86,47 +86,6 @@ const ENTITY_BANKACCOUNT_PLAIN_FIELDS = {
const GLOBAL_MIGRATION_TABLES = new Set(["accounts", "units", "citys", "countrys"])
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
const matrixServerName = () =>
process.env.MATRIX_SERVER_NAME ||
secrets.MATRIX_SERVER_NAME ||
process.env.DOMAIN ||
"localhost"
const normalizeMatrixLocalpartSeed = (value: string) => {
const normalized = value
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ä/g, "a")
.replace(/ö/g, "o")
.replace(/ü/g, "u")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9._=-]+/g, "_")
.replace(/_+/g, "_")
.replace(/^[._=-]+|[._=-]+$/g, "")
return normalized || "user"
}
const normalizeMatrixAliasSeed = (value: string) =>
normalizeMatrixLocalpartSeed(value)
.replace(/[.=]/g, "_")
.replace(/_+/g, "_")
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)}:${matrixServerName()}`
const tableColumns = async (client: any) => {
const result = await client.query(`
select table_name, column_name, data_type, is_generated
@@ -343,6 +302,12 @@ export const buildTenantFullExport = async (
addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds]))
}
const communicationRoomIds = collectIds(tables.communication_rooms || [], "id")
if (communicationRoomIds.length) {
addRows(tables, "communication_room_members", await loadRows(client, "communication_room_members", "room_id = any($1::uuid[])", [communicationRoomIds]))
addRows(tables, "communication_room_reads", await loadRows(client, "communication_room_reads", "room_id = any($1::uuid[])", [communicationRoomIds]))
}
if (tables.entitybankaccounts?.length) {
tables.entitybankaccounts = decryptEntityBankAccountsForExport(tables.entitybankaccounts)
}
@@ -636,73 +601,6 @@ const encryptEntityBankAccountRowsForImport = (exportData: TenantFullExport) =>
}
}
const prepareCommunicationRoomsForImport = (exportData: TenantFullExport) => {
const rows = exportData.tables.communication_rooms || []
if (!rows.length) return
const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
Number(tenant.id),
{
id: Number(tenant.id),
name: tenant.name,
short: tenant.short,
},
]))
for (const row of rows) {
const tenantId = Number(row.tenant_id)
const tenant = tenantById.get(tenantId)
row.matrix_room_id = null
row.parent_space_room_id = null
if (tenant && row.key) {
row.matrix_alias = tenantRoomAlias(tenant, String(row.key))
} else {
row.matrix_alias = null
}
}
}
const cleanupImportedCommunicationRooms = async (client: any, exportData: TenantFullExport) => {
const rows = exportData.tables.communication_rooms || []
if (!rows.length) return 0
const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
Number(tenant.id),
{
id: Number(tenant.id),
name: tenant.name,
short: tenant.short,
},
]))
let cleaned = 0
for (const row of rows) {
const tenantId = Number(row.tenant_id)
const key = String(row.key || "")
const tenant = tenantById.get(tenantId)
if (!tenantId || !key || !tenant) continue
const alias = tenantRoomAlias(tenant, key)
const result = await client.query(
`
update communication_rooms
set matrix_room_id = null,
parent_space_room_id = null,
matrix_alias = $3,
updated_at = now()
where tenant_id = $1 and key = $2
`,
[tenantId, key, alias]
)
cleaned += result.rowCount || 0
}
return cleaned
}
const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
if (!isJsonColumn || value === null || typeof value === "undefined") return value
if (typeof value === "string") return value
@@ -853,7 +751,6 @@ export const importTenantFullExport = async (
const exportData = rawExportData
encryptEntityBankAccountRowsForImport(exportData)
prepareCommunicationRoomsForImport(exportData)
const client = await pool.connect()
const importOrder = [
"tenants",
@@ -955,13 +852,6 @@ export const importTenantFullExport = async (
await reportProgress(`${table} importiert`)
}
const cleanedCommunicationRooms = await cleanupImportedCommunicationRooms(client, exportData)
if (cleanedCommunicationRooms) {
importedTables.push({ table: "communication_rooms_matrix_reset", rows: cleanedCommunicationRooms })
}
progressDone += 1
await reportProgress("Kommunikationsräume bereinigt")
await refreshSequences(client, columnsByTable)
progressDone = progressTotal
await reportProgress("Import abgeschlossen")