KI-AGENT: Live-Sync und Nachrichtenaktionen im Chat ergänzen

This commit is contained in:
2026-05-20 20:27:38 +02:00
parent a671ae392d
commit 1a5c69fcfb
3 changed files with 496 additions and 14 deletions

View File

@@ -17,6 +17,7 @@ type MatrixRoomEvent = {
sender: string
origin_server_ts: number
type: string
redacts?: string
content?: {
body?: string
msgtype?: string
@@ -60,6 +61,20 @@ type MatrixRoomSearchResponse = {
}
}
type MatrixSyncResponse = {
next_batch?: string
rooms?: {
join?: Record<string, {
timeline?: {
events?: MatrixRoomEvent[]
}
state?: {
events?: MatrixRoomEvent[]
}
}>
}
}
type MatrixUserSession = {
accessToken: string
matrixUserId: string
@@ -1370,6 +1385,133 @@ export function matrixService(server: FastifyInstance) {
}
}
const syncTenantRoomEvents = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
since?: string,
initial = false
) => {
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
const filter = {
room: {
rooms: [room.roomId],
timeline: {
limit: 30,
},
},
presence: {
types: [],
},
account_data: {
types: [],
},
}
const params = new URLSearchParams({
timeout: since && !initial ? "25000" : "0",
filter: JSON.stringify(filter),
})
if (since) params.set("since", since)
const response = await requestMatrixJson<MatrixSyncResponse>(
`/_matrix/client/v3/sync?${params.toString()}`,
session.accessToken
)
const joinedRoom = response.rooms?.join?.[room.roomId]
const timelineEvents = joinedRoom?.timeline?.events || []
const stateEvents = joinedRoom?.state?.events || []
if (initial) {
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
nextBatch: response.next_batch || since || "",
messages: [],
replacements: [],
reactions: [],
redactions: [],
membersChanged: false,
}
}
const messages = timelineEvents
.filter((event) =>
event.type === "m.room.message" &&
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
event.content?.["m.relates_to"]?.rel_type !== "m.replace"
)
.map((event) => ({
id: event.event_id,
sender: event.sender,
senderDisplayName: event.sender,
body: event.content?.body || "",
attachment: attachmentFromEvent(event),
timestamp: event.origin_server_ts,
own: event.sender === session.matrixUserId,
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
reactions: [],
}))
const replacements = timelineEvents
.filter((event) =>
event.type === "m.room.message" &&
event.content?.["m.relates_to"]?.rel_type === "m.replace" &&
Boolean(event.content?.["m.relates_to"]?.event_id)
)
.map((event) => ({
id: event.event_id,
targetEventId: event.content?.["m.relates_to"]?.event_id,
body: event.content?.["m.new_content"]?.body || event.content?.body || "",
timestamp: event.origin_server_ts,
sender: event.sender,
own: event.sender === session.matrixUserId,
}))
const reactions = timelineEvents
.filter((event) =>
event.type === "m.reaction" &&
event.content?.["m.relates_to"]?.rel_type === "m.annotation" &&
Boolean(event.content?.["m.relates_to"]?.event_id) &&
Boolean(event.content?.["m.relates_to"]?.key)
)
.map((event) => ({
id: event.event_id,
targetEventId: event.content?.["m.relates_to"]?.event_id,
key: event.content?.["m.relates_to"]?.key,
sender: event.sender,
own: event.sender === session.matrixUserId,
}))
const redactions = timelineEvents
.filter((event) => event.type === "m.room.redaction" && Boolean(event.redacts))
.map((event) => ({
id: event.event_id,
targetEventId: event.redacts,
sender: event.sender,
timestamp: event.origin_server_ts,
}))
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
nextBatch: response.next_batch || since || "",
messages,
replacements,
reactions,
redactions,
membersChanged: [...timelineEvents, ...stateEvents].some((event) => event.type === "m.room.member"),
}
}
const sendTenantRoomMessage = async (
userId: string,
tenantId: number | null,
@@ -1472,6 +1614,91 @@ export function matrixService(server: FastifyInstance) {
return { success: true, eventId, key: reactionKey }
}
const editTenantRoomMessage = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
eventId: string,
text: string
) => {
const message = text.trim()
if (!eventId || !message) {
throw Object.assign(
new Error("Nachricht und Zielnachricht sind erforderlich"),
{ statusCode: 400 }
)
}
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
const response = await requestMatrixJson<{ event_id: string }>(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
session.accessToken,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
msgtype: "m.text",
body: `* ${message}`,
"m.new_content": {
msgtype: "m.text",
body: message,
},
"m.relates_to": {
rel_type: "m.replace",
event_id: eventId,
},
}),
}
)
return {
id: response.event_id,
targetEventId: eventId,
body: message,
timestamp: Date.now(),
own: true,
}
}
const redactTenantRoomMessage = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
eventId: string
) => {
if (!eventId) {
throw Object.assign(
new Error("Zielnachricht ist erforderlich"),
{ statusCode: 400 }
)
}
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
await requestMatrixJson(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`,
session.accessToken,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
reason: "Nachricht in FEDEO gelöscht",
}),
}
)
return { success: true, eventId }
}
const markTenantRoomRead = async (
userId: string,
tenantId: number | null,
@@ -1961,8 +2188,11 @@ export function matrixService(server: FastifyInstance) {
getTenantRoomMessages,
getTenantRoomMembers,
searchTenantRoomMessages,
syncTenantRoomEvents,
sendTenantRoomMessage,
sendTenantRoomReaction,
editTenantRoomMessage,
redactTenantRoomMessage,
markTenantRoomRead,
sendTenantRoomAttachment,
getMediaContent,

View File

@@ -736,6 +736,21 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.get("/communication/matrix/rooms/:roomKey/sync", async (req, reply) => {
try {
const query = req.query as { since?: string; initial?: string }
return await matrix.syncTenantRoomEvents(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
query.since,
query.initial === "1"
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix sync failed")
}
})
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
try {
return await matrix.getTenantRoomMembers(
@@ -851,6 +866,36 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.put("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
try {
const params = req.params as { eventId: string }
const body = req.body as { text?: string }
return await matrix.editTenantRoomMessage(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
params.eventId,
body.text || ""
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix message edit failed")
}
})
server.delete("/communication/matrix/rooms/:roomKey/messages/:eventId", async (req, reply) => {
try {
const params = req.params as { eventId: string }
return await matrix.redactTenantRoomMessage(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
params.eventId
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix message delete failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
try {
const attachment = await uploadedAttachmentFromRequest(req)