KI-AGENT: Mitgliederverwaltung und Suche im Chat umsetzen

This commit is contained in:
2026-05-20 20:21:18 +02:00
parent 4c58d175a0
commit a671ae392d
3 changed files with 472 additions and 4 deletions

View File

@@ -47,6 +47,19 @@ type MatrixJoinedMembersResponse = {
}>
}
type MatrixRoomSearchResponse = {
search_categories?: {
room_events?: {
count?: number
results?: Array<{
result?: MatrixRoomEvent & {
room_id?: string
}
}>
}
}
}
type MatrixUserSession = {
accessToken: string
matrixUserId: string
@@ -1273,6 +1286,90 @@ export function matrixService(server: FastifyInstance) {
}
}
const searchTenantRoomMessages = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
query: string
) => {
const searchTerm = query.trim()
if (searchTerm.length < 2) {
return {
roomId: "",
alias: "",
key: options.key || "allgemein",
name: options.name || options.key || "Chat",
count: 0,
results: [],
}
}
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
const [response, members] = await Promise.all([
requestMatrixJson<MatrixRoomSearchResponse>(
"/_matrix/client/v3/search",
session.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
search_categories: {
room_events: {
search_term: searchTerm,
keys: ["content.body"],
order_by: "recent",
filter: {
limit: 25,
rooms: [room.roomId],
},
},
},
}),
}
),
requestMatrixJson<MatrixJoinedMembersResponse>(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
session.accessToken
),
])
const roomEvents = response.search_categories?.room_events
const results = (roomEvents?.results || [])
.map((item) => item.result)
.filter((event): event is MatrixRoomEvent & { room_id?: string } =>
Boolean(
event?.event_id &&
event.type === "m.room.message" &&
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
)
)
.map((event) => ({
id: event.event_id,
roomId: event.room_id || room.roomId,
key: room.key,
sender: event.sender,
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
body: event.content?.body || "",
attachment: attachmentFromEvent(event),
timestamp: event.origin_server_ts,
own: event.sender === session.matrixUserId,
}))
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
count: roomEvents?.count || results.length,
results,
}
}
const sendTenantRoomMessage = async (
userId: string,
tenantId: number | null,
@@ -1608,13 +1705,18 @@ export function matrixService(server: FastifyInstance) {
))
.where(eq(authTenantUsers.tenant_id, tenant.id))
return rows
const users = rows
.filter((row) => row.profileActive !== false)
.map((row) => ({
userId: row.userId,
email: row.email,
displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
}))
return await Promise.all(users.map(async (user) => ({
...user,
matrixUserId: await matrixUserIdForUser(user.userId, tenant.id),
})))
}
const inviteMatrixUserToRoom = async (
@@ -1708,6 +1810,102 @@ export function matrixService(server: FastifyInstance) {
}
}
const inviteTenantRoomMember = async (
requestingUserId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
targetUserId: string
) => {
if (!targetUserId) {
throw Object.assign(
new Error("Benutzer ist erforderlich"),
{ statusCode: 400 }
)
}
const users = await listTenantCommunicationUsers(tenantId)
const targetUser = users.find((user) => user.userId === targetUserId)
if (!targetUser) {
throw Object.assign(
new Error("Benutzer gehört nicht zum aktiven Mandanten"),
{ statusCode: 404 }
)
}
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
const account = await provisionCurrentUser(targetUser.userId, tenantId)
const inviteStatus = await inviteMatrixUserToRoom(
{ roomId: room.roomId },
account.matrixUserId,
`FEDEO-Einladung: ${room.name}`
)
await ensureCurrentUserJoinedRoom(targetUser.userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
userId: targetUser.userId,
email: targetUser.email,
displayName: targetUser.displayName,
matrixUserId: account.matrixUserId,
status: inviteStatus === "invited" ? "joined" : inviteStatus,
ok: true,
}
}
const removeTenantRoomMember = async (
requestingUserId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
matrixUserId: string
) => {
if (!matrixUserId) {
throw Object.assign(
new Error("Matrix-Benutzer ist erforderlich"),
{ statusCode: 400 }
)
}
const room = await provisionTenantRoom(requestingUserId, tenantId, options)
const session = await createAccessTokenForUser(requestingUserId, tenantId)
if (matrixUserId === session.matrixUserId) {
throw Object.assign(
new Error("Du kannst dich nicht selbst aus dem Raum entfernen"),
{ statusCode: 400 }
)
}
const serviceLogin = await ensureServiceAccessToken()
await requestMatrixJson(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/kick`,
serviceLogin.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: matrixUserId,
reason: "FEDEO-Mitgliederverwaltung",
}),
}
)
matrixJoinedRoomCache.delete(`${matrixUserId}:${room.roomId}`)
return {
success: true,
roomId: room.roomId,
alias: room.alias,
key: room.key,
matrixUserId,
}
}
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
getTenantRoomMessages(userId, tenantId, {
key: "allgemein",
@@ -1756,11 +1954,13 @@ export function matrixService(server: FastifyInstance) {
getTenantSpaceStatus,
provisionCurrentTenantSpace,
listTenantRooms,
listTenantCommunicationUsers,
getTenantRoomStatus,
provisionTenantRoom,
createAccessTokenForUser,
getTenantRoomMessages,
getTenantRoomMembers,
searchTenantRoomMessages,
sendTenantRoomMessage,
sendTenantRoomReaction,
markTenantRoomRead,
@@ -1768,6 +1968,8 @@ export function matrixService(server: FastifyInstance) {
getMediaContent,
createElementRoomSession,
createLiveKitRoomSession,
inviteTenantRoomMember,
removeTenantRoomMember,
syncTenantRoomMembers,
getGeneralRoomMessages,
getGeneralRoomMembers,

View File

@@ -603,6 +603,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.get("/communication/matrix/users", async (req, reply) => {
try {
const users = await matrix.listTenantCommunicationUsers(req.user.tenant_id)
return { users }
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix users failed")
}
})
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
try {
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
@@ -713,6 +722,20 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.get("/communication/matrix/rooms/:roomKey/search", async (req, reply) => {
try {
const query = req.query as { q?: string }
return await matrix.searchTenantRoomMessages(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
query.q || ""
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix search failed")
}
})
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
try {
return await matrix.getTenantRoomMembers(
@@ -725,6 +748,34 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.post("/communication/matrix/rooms/:roomKey/members/invite", async (req, reply) => {
try {
const body = req.body as { userId?: string }
return await matrix.inviteTenantRoomMember(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
body.userId || ""
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix member invite failed")
}
})
server.delete("/communication/matrix/rooms/:roomKey/members/:matrixUserId", async (req, reply) => {
try {
const params = req.params as { matrixUserId: string }
return await matrix.removeTenantRoomMember(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
params.matrixUserId
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix member remove failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
try {
return await matrix.createElementRoomSession(