KI-AGENT: Chat Anhänge und Nachrichteninteraktionen abrunden

This commit is contained in:
2026-05-20 20:12:02 +02:00
parent bc655f0e06
commit 4c58d175a0
3 changed files with 402 additions and 44 deletions

View File

@@ -25,6 +25,18 @@ type MatrixRoomEvent = {
mimetype?: string
size?: number
}
"m.new_content"?: {
body?: string
msgtype?: string
}
"m.relates_to"?: {
event_id?: string
key?: string
rel_type?: string
"m.in_reply_to"?: {
event_id?: string
}
}
}
}
@@ -71,6 +83,10 @@ type MatrixAttachmentInput = {
size: number
}
type MatrixMessageOptions = {
replyToEventId?: string
}
type MatrixCachedValue<T = any> = {
exists: true
cachedUntil: number
@@ -1158,27 +1174,72 @@ export function matrixService(server: FastifyInstance) {
session.accessToken
)
const replacementByEventId = new Map<string, MatrixRoomEvent>()
const reactionsByEventId = new Map<string, Map<string, { key: string; count: number; own: boolean }>>()
for (const event of response.chunk) {
const relation = event.content?.["m.relates_to"]
if (
event.type === "m.room.message" &&
relation?.rel_type === "m.replace" &&
relation.event_id
) {
replacementByEventId.set(relation.event_id, event)
}
if (
event.type === "m.reaction" &&
relation?.rel_type === "m.annotation" &&
relation.event_id &&
relation.key
) {
const eventReactions = reactionsByEventId.get(relation.event_id) || new Map()
const reaction = eventReactions.get(relation.key) || {
key: relation.key,
count: 0,
own: false,
}
reaction.count += 1
reaction.own = reaction.own || event.sender === session.matrixUserId
eventReactions.set(relation.key, reaction)
reactionsByEventId.set(relation.event_id, eventReactions)
}
}
const messages = response.chunk
.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) => {
const replacement = replacementByEventId.get(event.event_id)
const content = replacement?.content?.["m.new_content"] || replacement?.content || event.content
return {
id: event.event_id,
sender: event.sender,
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
body: content?.body || "",
attachment: attachmentFromEvent({ ...event, content }),
timestamp: replacement?.origin_server_ts || event.origin_server_ts,
own: event.sender === session.matrixUserId,
edited: Boolean(replacement),
replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
reactions: Array.from(reactionsByEventId.get(event.event_id)?.values() || []),
}
})
.reverse()
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
matrixUserId: session.matrixUserId,
messages: response.chunk
.filter((event) =>
event.type === "m.room.message" &&
["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
)
.map((event) => ({
id: event.event_id,
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,
}))
.reverse(),
messages,
}
}
@@ -1216,7 +1277,8 @@ export function matrixService(server: FastifyInstance) {
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
text: string
text: string,
messageOptions: MatrixMessageOptions = {}
) => {
const message = text.trim()
@@ -1235,16 +1297,26 @@ export function matrixService(server: FastifyInstance) {
})
const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
const content: Record<string, any> = {
msgtype: "m.text",
body: message,
}
if (messageOptions.replyToEventId) {
content["m.relates_to"] = {
"m.in_reply_to": {
event_id: messageOptions.replyToEventId,
},
}
}
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,
}),
body: JSON.stringify(content),
}
)
@@ -1258,9 +1330,78 @@ export function matrixService(server: FastifyInstance) {
roomId: room.roomId,
alias: room.alias,
key: room.key,
replyToEventId: messageOptions.replyToEventId || null,
reactions: [],
}
}
const sendTenantRoomReaction = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
eventId: string,
key: string
) => {
const reactionKey = key.trim()
if (!eventId || !reactionKey) {
throw Object.assign(
new Error("Reaction target and key are required"),
{ 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<{ event_id: string }>(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.reaction/${encodeURIComponent(txnId)}`,
session.accessToken,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"m.relates_to": {
rel_type: "m.annotation",
event_id: eventId,
key: reactionKey,
},
}),
}
)
return { success: true, eventId, key: reactionKey }
}
const markTenantRoomRead = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {},
eventId: string
) => {
if (!eventId) return { success: true, skipped: true }
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
await requestMatrixJson(
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/receipt/m.read/${encodeURIComponent(eventId)}`,
session.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}
)
return { success: true, eventId }
}
const sendTenantRoomAttachment = async (
userId: string,
tenantId: number | null,
@@ -1579,7 +1720,12 @@ export function matrixService(server: FastifyInstance) {
name: "Allgemeiner Chat",
})
const sendGeneralRoomMessage = (userId: string, tenantId: number | null, text: string) =>
const sendGeneralRoomMessage = (
userId: string,
tenantId: number | null,
text: string,
messageOptions: MatrixMessageOptions = {}
) =>
sendTenantRoomMessage(
userId,
tenantId,
@@ -1587,7 +1733,8 @@ export function matrixService(server: FastifyInstance) {
key: "allgemein",
name: "Allgemeiner Chat",
},
text
text,
messageOptions
)
const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
@@ -1615,6 +1762,8 @@ export function matrixService(server: FastifyInstance) {
getTenantRoomMessages,
getTenantRoomMembers,
sendTenantRoomMessage,
sendTenantRoomReaction,
markTenantRoomRead,
sendTenantRoomAttachment,
getMediaContent,
createElementRoomSession,

View File

@@ -641,8 +641,10 @@ export default async function communicationRoutes(server: FastifyInstance) {
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
try {
const body = req.body as { text?: string }
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "")
const body = req.body as { text?: string; replyToEventId?: string }
const message = await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "", {
replyToEventId: body.replyToEventId,
})
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
return message
@@ -688,7 +690,12 @@ export default async function communicationRoutes(server: FastifyInstance) {
try {
if (!req.user.tenant_id) return reply.code(400).send({ error: "Kein aktiver Mandant" })
const params = req.params as { roomKey: string }
return await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
const body = (req.body || {}) as { eventId?: string }
const result = await markRoomNotificationsRead(req.user.tenant_id, req.user.user_id, params.roomKey)
if (body.eventId) {
await matrix.markTenantRoomRead(req.user.user_id, req.user.tenant_id, roomOptionsFromRequest(req), body.eventId)
}
return result
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix room read state failed")
}
@@ -759,12 +766,15 @@ export default async function communicationRoutes(server: FastifyInstance) {
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
try {
const body = req.body as { text?: string }
const body = req.body as { text?: string; replyToEventId?: string }
const message = await matrix.sendTenantRoomMessage(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
body.text || ""
body.text || "",
{
replyToEventId: body.replyToEventId,
}
)
const room = await matrix.getTenantRoomStatus(req.user.tenant_id, message.key)
await notifyUsersAboutChatMessage(req, room, message, body.text || "")
@@ -774,6 +784,22 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.post("/communication/matrix/rooms/:roomKey/messages/:eventId/reactions", async (req, reply) => {
try {
const params = req.params as { eventId: string }
const body = req.body as { key?: string }
return await matrix.sendTenantRoomReaction(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
params.eventId,
body.key || ""
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix reaction send failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/attachments", async (req, reply) => {
try {
const attachment = await uploadedAttachmentFromRequest(req)