KI-AGENT: Nativen Matrix-Chat in FEDEO starten
This commit is contained in:
@@ -11,6 +11,17 @@ type MatrixErrorResponse = {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixRoomEvent = {
|
||||||
|
event_id: string
|
||||||
|
sender: string
|
||||||
|
origin_server_ts: number
|
||||||
|
type: string
|
||||||
|
content?: {
|
||||||
|
body?: string
|
||||||
|
msgtype?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
|
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
|
||||||
const readLocalDevRegistrationSharedSecret = () => {
|
const readLocalDevRegistrationSharedSecret = () => {
|
||||||
if (process.env.NODE_ENV === "production") return ""
|
if (process.env.NODE_ENV === "production") return ""
|
||||||
@@ -277,6 +288,30 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createAccessTokenForUser = async (userId: string, tenantId: number | null) => {
|
||||||
|
await provisionCurrentUser(userId, tenantId)
|
||||||
|
|
||||||
|
const matrixUserId = await matrixUserIdForUser(userId, tenantId)
|
||||||
|
const serviceLogin = await ensureServiceAccessToken()
|
||||||
|
const validUntilMs = Date.now() + 10 * 60 * 1000
|
||||||
|
|
||||||
|
const login = await requestMatrixJson<{ access_token: string }>(
|
||||||
|
`/_synapse/admin/v1/users/${encodeURIComponent(matrixUserId)}/login`,
|
||||||
|
serviceLogin.access_token,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ valid_until_ms: validUntilMs }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: login.access_token,
|
||||||
|
matrixUserId,
|
||||||
|
validUntilMs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getStatus = async () => {
|
const getStatus = async () => {
|
||||||
const configured = Boolean(homeserverUrl() && serverName())
|
const configured = Boolean(homeserverUrl() && serverName())
|
||||||
|
|
||||||
@@ -607,6 +642,118 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureCurrentUserJoinedRoom = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
room: { roomId: string, alias: string }
|
||||||
|
) => {
|
||||||
|
const session = await createAccessTokenForUser(userId, tenantId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestMatrixJson(
|
||||||
|
`/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`,
|
||||||
|
session.accessToken,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.errcode !== "M_FORBIDDEN" && err.errcode !== "M_UNKNOWN") {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGeneralRoomMessages = async (userId: string, tenantId: number | null) => {
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, {
|
||||||
|
key: "allgemein",
|
||||||
|
name: "Allgemeiner Chat",
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await requestMatrixJson<{
|
||||||
|
chunk: MatrixRoomEvent[]
|
||||||
|
start?: string
|
||||||
|
end?: string
|
||||||
|
}>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/messages?dir=b&limit=50`,
|
||||||
|
session.accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
matrixUserId: session.matrixUserId,
|
||||||
|
messages: response.chunk
|
||||||
|
.filter((event) => event.type === "m.room.message" && event.content?.msgtype === "m.text")
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.event_id,
|
||||||
|
sender: event.sender,
|
||||||
|
body: event.content?.body || "",
|
||||||
|
timestamp: event.origin_server_ts,
|
||||||
|
own: event.sender === session.matrixUserId,
|
||||||
|
}))
|
||||||
|
.reverse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendGeneralRoomMessage = async (
|
||||||
|
userId: string,
|
||||||
|
tenantId: number | null,
|
||||||
|
text: string
|
||||||
|
) => {
|
||||||
|
const message = text.trim()
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Message text is required"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, {
|
||||||
|
key: "allgemein",
|
||||||
|
name: "Allgemeiner Chat",
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.event_id,
|
||||||
|
sender: session.matrixUserId,
|
||||||
|
body: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus,
|
getStatus,
|
||||||
matrixUserIdForUser,
|
matrixUserIdForUser,
|
||||||
@@ -616,5 +763,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
provisionCurrentTenantSpace,
|
provisionCurrentTenantSpace,
|
||||||
getTenantRoomStatus,
|
getTenantRoomStatus,
|
||||||
provisionTenantRoom,
|
provisionTenantRoom,
|
||||||
|
createAccessTokenForUser,
|
||||||
|
getGeneralRoomMessages,
|
||||||
|
sendGeneralRoomMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,4 +74,27 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
.send({ error: err.message || "Matrix room provisioning failed" })
|
.send({ error: err.message || "Matrix room provisioning failed" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.getGeneralRoomMessages(req.user.user_id, req.user.tenant_id)
|
||||||
|
} catch (err: any) {
|
||||||
|
req.log.error(err)
|
||||||
|
return reply
|
||||||
|
.code(err.statusCode || 500)
|
||||||
|
.send({ error: err.message || "Matrix messages failed" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const body = req.body as { text?: string }
|
||||||
|
return await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "")
|
||||||
|
} catch (err: any) {
|
||||||
|
req.log.error(err)
|
||||||
|
return reply
|
||||||
|
.code(err.statusCode || 500)
|
||||||
|
.send({ error: err.message || "Matrix message send failed" })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ const generalRoom = ref(null)
|
|||||||
const provisionResult = ref(null)
|
const provisionResult = ref(null)
|
||||||
const tenantSpaceProvisionResult = ref(null)
|
const tenantSpaceProvisionResult = ref(null)
|
||||||
const generalRoomProvisionResult = ref(null)
|
const generalRoomProvisionResult = ref(null)
|
||||||
|
const matrixMessages = ref([])
|
||||||
|
const matrixMessageDraft = ref("")
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
const tenantSpaceProvisioning = ref(false)
|
const tenantSpaceProvisioning = ref(false)
|
||||||
const generalRoomProvisioning = ref(false)
|
const generalRoomProvisioning = ref(false)
|
||||||
|
const matrixMessagesLoading = ref(false)
|
||||||
|
const matrixMessageSending = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
|
|
||||||
const statusItems = computed(() => [
|
const statusItems = computed(() => [
|
||||||
@@ -83,6 +87,10 @@ const loadMatrixInfo = async () => {
|
|||||||
tenantSpace.value = tenantSpaceRes
|
tenantSpace.value = tenantSpaceRes
|
||||||
generalRoom.value = generalRoomRes
|
generalRoom.value = generalRoomRes
|
||||||
lastUpdated.value = new Date()
|
lastUpdated.value = new Date()
|
||||||
|
|
||||||
|
if (generalRoomRes?.exists) {
|
||||||
|
await loadGeneralMessages()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Matrix-Status konnte nicht geladen werden",
|
title: "Matrix-Status konnte nicht geladen werden",
|
||||||
@@ -180,6 +188,8 @@ const provisionGeneralRoom = async () => {
|
|||||||
title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt",
|
title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt",
|
||||||
color: "success"
|
color: "success"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await loadGeneralMessages()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
||||||
@@ -190,6 +200,53 @@ const provisionGeneralRoom = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadGeneralMessages = async () => {
|
||||||
|
matrixMessagesLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await $api("/api/communication/matrix/rooms/general/messages")
|
||||||
|
matrixMessages.value = res.messages || []
|
||||||
|
generalRoom.value = {
|
||||||
|
...generalRoom.value,
|
||||||
|
alias: res.alias || generalRoom.value?.alias,
|
||||||
|
exists: true,
|
||||||
|
roomId: res.roomId || generalRoom.value?.roomId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Matrix-Nachrichten konnten nicht geladen werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matrixMessagesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMatrixMessage = async () => {
|
||||||
|
const text = matrixMessageDraft.value.trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
matrixMessageSending.value = true
|
||||||
|
try {
|
||||||
|
const message = await $api("/api/communication/matrix/rooms/general/messages", {
|
||||||
|
method: "POST",
|
||||||
|
body: { text }
|
||||||
|
})
|
||||||
|
|
||||||
|
matrixMessages.value = [
|
||||||
|
...matrixMessages.value,
|
||||||
|
message
|
||||||
|
]
|
||||||
|
matrixMessageDraft.value = ""
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Matrix-Nachricht konnte nicht gesendet werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matrixMessageSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatDateTime = (value) => {
|
const formatDateTime = (value) => {
|
||||||
if (!value) return "-"
|
if (!value) return "-"
|
||||||
|
|
||||||
@@ -199,6 +256,15 @@ const formatDateTime = (value) => {
|
|||||||
}).format(value)
|
}).format(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatMessageTime = (timestamp) => {
|
||||||
|
if (!timestamp) return ""
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat("de-DE", {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short"
|
||||||
|
}).format(new Date(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadMatrixInfo)
|
onMounted(loadMatrixInfo)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -504,31 +570,86 @@ onMounted(loadMatrixInfo)
|
|||||||
Matrix-Kommunikation
|
Matrix-Kommunikation
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 truncate text-xs text-muted">
|
<p class="mt-1 truncate text-xs text-muted">
|
||||||
{{ activeMatrixTarget?.name || activeMatrixTarget?.alias || "Element Web" }}
|
{{ generalRoom?.name || generalRoom?.alias || "Allgemeiner Chat" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UButton
|
<div class="flex flex-wrap gap-2">
|
||||||
icon="i-heroicons-arrow-top-right-on-square"
|
<UButton
|
||||||
color="neutral"
|
icon="i-heroicons-arrow-path"
|
||||||
variant="outline"
|
color="neutral"
|
||||||
:to="embeddedElementUrl"
|
variant="outline"
|
||||||
target="_blank"
|
:loading="matrixMessagesLoading"
|
||||||
>
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
||||||
Separat öffnen
|
@click="loadGeneralMessages"
|
||||||
</UButton>
|
>
|
||||||
|
Nachrichten laden
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-top-right-on-square"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
:to="embeddedElementUrl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Element öffnen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="h-[720px] min-h-[520px] bg-muted">
|
<div class="flex h-[640px] min-h-[520px] flex-col bg-muted">
|
||||||
<iframe
|
<div class="flex-1 space-y-3 overflow-y-auto p-4 sm:p-5">
|
||||||
class="h-full w-full border-0"
|
<div
|
||||||
:src="embeddedElementUrl"
|
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
||||||
title="Matrix-Kommunikation"
|
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
||||||
allow="camera; microphone; display-capture; clipboard-read; clipboard-write; autoplay; fullscreen"
|
>
|
||||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals allow-presentation"
|
Noch keine Nachrichten im allgemeinen Chat.
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="message in matrixMessages"
|
||||||
|
:key="message.id"
|
||||||
|
class="flex"
|
||||||
|
:class="message.own ? 'justify-end' : 'justify-start'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="max-w-[78%] rounded-lg px-3 py-2 shadow-sm"
|
||||||
|
:class="message.own ? 'bg-primary text-inverted' : 'bg-default text-highlighted'"
|
||||||
|
>
|
||||||
|
<div class="mb-1 flex items-center gap-2 text-[11px] opacity-75">
|
||||||
|
<span class="truncate font-medium">
|
||||||
|
{{ message.own ? "Du" : message.sender }}
|
||||||
|
</span>
|
||||||
|
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="whitespace-pre-wrap break-words text-sm">
|
||||||
|
{{ message.body }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex gap-2 border-t border-default bg-default p-3"
|
||||||
|
@submit.prevent="sendMatrixMessage"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="matrixMessageDraft"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
placeholder="Nachricht schreiben"
|
||||||
|
:disabled="matrixMessageSending || !status?.reachable || !status?.provisioningConfigured"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
icon="i-heroicons-paper-airplane"
|
||||||
|
:loading="matrixMessageSending"
|
||||||
|
:disabled="!matrixMessageDraft.trim() || !status?.reachable || !status?.provisioningConfigured"
|
||||||
|
>
|
||||||
|
Senden
|
||||||
|
</UButton>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user