KI-AGENT: Projekträume und Direktnachrichten integrieren
This commit is contained in:
@@ -56,6 +56,7 @@ type MatrixTenantRoomOptions = {
|
||||
entityType?: string | null
|
||||
entityId?: number | null
|
||||
entityUuid?: string | null
|
||||
inviteUserIds?: string[]
|
||||
}
|
||||
|
||||
type MatrixCachedValue<T = any> = {
|
||||
@@ -247,6 +248,7 @@ export function matrixService(server: FastifyInstance) {
|
||||
entityType: options.entityType || null,
|
||||
entityId: options.entityId || null,
|
||||
entityUuid: options.entityUuid || null,
|
||||
inviteUserIds: options.inviteUserIds || [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,6 +887,7 @@ export function matrixService(server: FastifyInstance) {
|
||||
|
||||
const existing = await getTenantRoomStatus(tenant.id, key, name)
|
||||
const userAccount = await provisionCurrentUser(userId, tenant.id)
|
||||
const invitedMatrixUserIds = await matrixUserIdsForInvitees(userId, tenant.id, normalizedOptions.inviteUserIds || [])
|
||||
const tenantSpace = await provisionCurrentTenantSpace(userId, tenant.id)
|
||||
|
||||
if (existing.exists) {
|
||||
@@ -902,6 +905,8 @@ export function matrixService(server: FastifyInstance) {
|
||||
invitedUserId: userAccount.matrixUserId,
|
||||
}
|
||||
|
||||
await inviteUsersToRoom(existing.roomId, invitedMatrixUserIds)
|
||||
|
||||
matrixTenantRoomCache.set(cacheKey, {
|
||||
exists: true,
|
||||
cachedUntil: Date.now() + 30 * 60 * 1000,
|
||||
@@ -923,7 +928,7 @@ export function matrixService(server: FastifyInstance) {
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
room_alias_name: tenantRoomAliasLocalpart(tenant, key),
|
||||
invite: [userAccount.matrixUserId],
|
||||
invite: Array.from(new Set([userAccount.matrixUserId, ...invitedMatrixUserIds])),
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
@@ -994,6 +999,42 @@ export function matrixService(server: FastifyInstance) {
|
||||
return value
|
||||
}
|
||||
|
||||
const matrixUserIdsForInvitees = async (
|
||||
currentUserId: string,
|
||||
tenantId: number,
|
||||
inviteUserIds: string[]
|
||||
) => {
|
||||
const uniqueUserIds = Array.from(new Set(inviteUserIds.filter((id) => id && id !== currentUserId)))
|
||||
|
||||
return await Promise.all(uniqueUserIds.map(async (inviteUserId) => {
|
||||
const account = await provisionCurrentUser(inviteUserId, tenantId)
|
||||
return account.matrixUserId
|
||||
}))
|
||||
}
|
||||
|
||||
const inviteUsersToRoom = async (roomId: string | null, matrixUserIds: string[]) => {
|
||||
if (!roomId || !matrixUserIds.length) return
|
||||
|
||||
const serviceLogin = await ensureServiceAccessToken()
|
||||
|
||||
for (const matrixUserId of matrixUserIds) {
|
||||
try {
|
||||
await requestMatrixJson(
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`,
|
||||
serviceLogin.accessToken,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: matrixUserId }),
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
if (err.statusCode === 403 || err.statusCode === 400) continue
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ensureCurrentUserJoinedRoom = async (
|
||||
userId: string,
|
||||
tenantId: number | null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, eq, ne } from "drizzle-orm"
|
||||
import { authTenantUsers, authUsers } from "../../db/schema"
|
||||
import { and, eq, inArray, ne } from "drizzle-orm"
|
||||
import { authProfiles, authTenantUsers, authUsers, projects } from "../../db/schema"
|
||||
import { matrixService } from "../modules/matrix.service"
|
||||
import { NotificationService, UserDirectory } from "../modules/notification.service"
|
||||
|
||||
@@ -47,6 +48,22 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
const projectRoomKey = (projectId: number) => `project_${projectId}`
|
||||
|
||||
const directRoomKey = (firstUserId: string, secondUserId: string) => {
|
||||
const hash = createHash("sha256")
|
||||
.update([firstUserId, secondUserId].sort().join(":"))
|
||||
.digest("hex")
|
||||
.slice(0, 16)
|
||||
|
||||
return `direct_${hash}`
|
||||
}
|
||||
|
||||
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 callModeFromRequest = (req: any): "audio" | "video" => {
|
||||
const body = (req.body || {}) as { mode?: string }
|
||||
return body.mode === "audio" ? "audio" : "video"
|
||||
@@ -131,6 +148,186 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/communication/matrix/project-rooms", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||
|
||||
const [roomsRes, projectRows] = await Promise.all([
|
||||
matrix.listTenantRooms(req.user.tenant_id),
|
||||
server.db
|
||||
.select({
|
||||
id: projects.id,
|
||||
name: projects.name,
|
||||
projectNumber: projects.projectNumber,
|
||||
profiles: projects.profiles,
|
||||
})
|
||||
.from(projects)
|
||||
.where(and(
|
||||
eq(projects.tenant, req.user.tenant_id),
|
||||
eq(projects.archived, false)
|
||||
))
|
||||
])
|
||||
const roomsByKey = new Map((roomsRes.rooms || []).map((room: any) => [room.key, room]))
|
||||
|
||||
return {
|
||||
rooms: projectRows.map((project) => {
|
||||
const key = projectRoomKey(project.id)
|
||||
const existing = roomsByKey.get(key) as any
|
||||
return {
|
||||
...(existing || {}),
|
||||
key,
|
||||
name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name,
|
||||
topic: `Projektkommunikation zu ${project.name}`,
|
||||
type: "project",
|
||||
entityType: "project",
|
||||
entityId: project.id,
|
||||
exists: Boolean(existing?.exists),
|
||||
projectId: project.id,
|
||||
projectNumber: project.projectNumber,
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
return handleMatrixError(req, reply, err, "Matrix project rooms failed")
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/communication/matrix/project-rooms/:projectId/provision", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||
const params = req.params as { projectId: string }
|
||||
const projectId = Number(params.projectId)
|
||||
const [project] = await server.db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(and(
|
||||
eq(projects.tenant, req.user.tenant_id),
|
||||
eq(projects.id, projectId)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
if (!project) return reply.code(404).send({ error: "Projekt nicht gefunden" })
|
||||
|
||||
const profileIds = (project.profiles || []) as string[]
|
||||
const profileRows = profileIds.length
|
||||
? await server.db
|
||||
.select({ userId: authProfiles.user_id })
|
||||
.from(authProfiles)
|
||||
.where(and(
|
||||
eq(authProfiles.tenant_id, req.user.tenant_id),
|
||||
inArray(authProfiles.id, profileIds)
|
||||
))
|
||||
: []
|
||||
const inviteUserIds = profileRows.map((profile) => profile.userId).filter(Boolean) as string[]
|
||||
|
||||
return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, {
|
||||
key: projectRoomKey(project.id),
|
||||
name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name,
|
||||
topic: `Projektkommunikation zu ${project.name}`,
|
||||
type: "project",
|
||||
entityType: "project",
|
||||
entityId: project.id,
|
||||
inviteUserIds,
|
||||
})
|
||||
} catch (err: any) {
|
||||
return handleMatrixError(req, reply, err, "Matrix project room provisioning failed")
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/communication/matrix/direct-rooms", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||
const [roomsRes, userRows] = await Promise.all([
|
||||
matrix.listTenantRooms(req.user.tenant_id),
|
||||
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, req.user.tenant_id)
|
||||
))
|
||||
.where(and(
|
||||
eq(authTenantUsers.tenant_id, req.user.tenant_id),
|
||||
ne(authTenantUsers.user_id, req.user.user_id)
|
||||
))
|
||||
])
|
||||
const roomsByKey = new Map((roomsRes.rooms || []).map((room: any) => [room.key, room]))
|
||||
|
||||
return {
|
||||
rooms: userRows.map((user) => {
|
||||
const key = directRoomKey(req.user.user_id, user.userId)
|
||||
const existing = roomsByKey.get(key) as any
|
||||
const name = displayUserName(user)
|
||||
|
||||
return {
|
||||
...(existing || {}),
|
||||
key,
|
||||
name,
|
||||
topic: `Direktnachricht mit ${name}`,
|
||||
type: "direct",
|
||||
entityType: "user",
|
||||
entityUuid: user.userId,
|
||||
exists: Boolean(existing?.exists),
|
||||
userId: user.userId,
|
||||
email: user.email,
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
return handleMatrixError(req, reply, err, "Matrix direct rooms failed")
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/communication/matrix/direct-rooms/:userId/provision", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
|
||||
const params = req.params as { userId: string }
|
||||
const [target] = 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, req.user.tenant_id)
|
||||
))
|
||||
.where(and(
|
||||
eq(authTenantUsers.tenant_id, req.user.tenant_id),
|
||||
eq(authTenantUsers.user_id, params.userId)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
if (!target || target.userId === req.user.user_id) {
|
||||
return reply.code(404).send({ error: "Benutzer nicht gefunden" })
|
||||
}
|
||||
|
||||
const targetName = displayUserName(target)
|
||||
return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, {
|
||||
key: directRoomKey(req.user.user_id, target.userId),
|
||||
name: targetName,
|
||||
topic: `Direktnachricht mit ${targetName}`,
|
||||
type: "direct",
|
||||
entityType: "user",
|
||||
entityUuid: target.userId,
|
||||
inviteUserIds: [target.userId],
|
||||
})
|
||||
} catch (err: any) {
|
||||
return handleMatrixError(req, reply, err, "Matrix direct room provisioning failed")
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/communication/matrix/rooms", async (req, reply) => {
|
||||
try {
|
||||
return await matrix.provisionTenantRoom(
|
||||
|
||||
Reference in New Issue
Block a user