feat: chat E2EE MVP - rooms list + timeline + composer + sync (issues 0148+0149+0150)
Backend extends MatrixService with Start()/Stop()/ListRooms()/LoadTimeline()/
SendText()/SendMarkdown(). On login the service initialises the crypto store
(cryptohelper, Olm/Megolm via goolm build tag) and a sync loop that fans
events out through Wails events ("matrix:event", "matrix:error"). Pickle
key is 32 random bytes hex-encoded in the OS keyring alongside the access
token, so the crypto SQLite store survives restarts.
Vendors 4 fresh helpers from fn_registry/functions/infra/:
matrix_crypto_init.go (//go:build goolm || libolm)
matrix_sync_service.go
matrix_message_send.go
matrix_room_list.go
Plus the existing 3 (mas_oidc_loopback, keyring_token_store, matrix_client_init).
go-sqlite3 driver pulled explicitly via sqlite_driver.go.
Frontend rewires HomeScreen as a 3-zone AppShell (sidebar / timeline /
composer). useMatrixRooms polls + reacts to the sync stream; useMatrixTimeline
loads the last 50 events of the selected room and appends live ones. New
components: RoomList, Timeline, EventBubble, Composer. Composer supports
plain text (default) and a markdown toggle; Enter sends, Shift+Enter newline.
wails.json now passes "build:tags": "goolm" by default. Tested with
wails build -tags goolm on linux/amd64 and windows/amd64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/yuin/goldmark"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// matrixMarkdownToHTML convierte Markdown a HTML sanitizado con goldmark + bluemonday.
|
||||
// El HTML resultante es seguro para incluir en formatted_body de un evento Matrix.
|
||||
// Allowlist: bluemonday UGCPolicy + <details>, <summary>, <code>, <pre>.
|
||||
func matrixMarkdownToHTML(markdown string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := goldmark.Convert([]byte(markdown), &buf); err != nil {
|
||||
return "", fmt.Errorf("matrix_message_send: goldmark convert: %w", err)
|
||||
}
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowElements("details", "summary", "code", "pre")
|
||||
sanitized := p.SanitizeBytes(buf.Bytes())
|
||||
return string(sanitized), nil
|
||||
}
|
||||
|
||||
// matrixSendEvent es el helper interno que llama a client.SendMessageEvent
|
||||
// y devuelve el id.EventID asignado por Synapse.
|
||||
func matrixSendEvent(ctx context.Context, client *mautrix.Client, roomID id.RoomID, eventType event.Type, content interface{}) (id.EventID, error) {
|
||||
resp, err := client.SendMessageEvent(ctx, roomID, eventType, content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.EventID, nil
|
||||
}
|
||||
|
||||
// MatrixSendText envía un mensaje de texto plano (m.text) al room indicado.
|
||||
// Si el room tiene E2EE activo y client.Crypto != nil, mautrix cifra automáticamente.
|
||||
func MatrixSendText(ctx context.Context, client *mautrix.Client, roomID id.RoomID, body string) (id.EventID, error) {
|
||||
if client == nil {
|
||||
return "", fmt.Errorf("matrix_message_send: client no puede ser nil")
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: body,
|
||||
}
|
||||
return matrixSendEvent(ctx, client, roomID, event.EventMessage, content)
|
||||
}
|
||||
|
||||
// MatrixSendMarkdown convierte markdown a HTML con goldmark, lo sanitiza con bluemonday
|
||||
// (UGCPolicy + <details>, <summary>, <code>, <pre>) y envía con format=org.matrix.custom.html.
|
||||
// El campo Body contiene el markdown original como fallback para clientes sin HTML.
|
||||
func MatrixSendMarkdown(ctx context.Context, client *mautrix.Client, roomID id.RoomID, markdown string) (id.EventID, error) {
|
||||
if client == nil {
|
||||
return "", fmt.Errorf("matrix_message_send: client no puede ser nil")
|
||||
}
|
||||
htmlBody, err := matrixMarkdownToHTML(markdown)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("matrix_message_send.MatrixSendMarkdown: %w", err)
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: markdown,
|
||||
Format: event.FormatHTML,
|
||||
FormattedBody: htmlBody,
|
||||
}
|
||||
return matrixSendEvent(ctx, client, roomID, event.EventMessage, content)
|
||||
}
|
||||
|
||||
// MatrixSendReply envía un mensaje con m.relates_to.m.in_reply_to apuntando a replyTo.
|
||||
// El body es el texto de la respuesta. En v0.1.0 el caller construye la cita si la necesita.
|
||||
// El cifrado E2EE es automático si client.Crypto está configurado.
|
||||
func MatrixSendReply(ctx context.Context, client *mautrix.Client, roomID id.RoomID, replyTo id.EventID, body string) (id.EventID, error) {
|
||||
if client == nil {
|
||||
return "", fmt.Errorf("matrix_message_send: client no puede ser nil")
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: body,
|
||||
RelatesTo: (&event.RelatesTo{}).SetReplyTo(replyTo),
|
||||
}
|
||||
return matrixSendEvent(ctx, client, roomID, event.EventMessage, content)
|
||||
}
|
||||
|
||||
// MatrixEditMessage envía un replacement event (m.replace) compatible con Element y la spec Matrix.
|
||||
// NewContent contiene el texto nuevo; Body es el fallback "* newBody" para clientes sin soporte de edición.
|
||||
// eventID es el evento original a reemplazar.
|
||||
func MatrixEditMessage(ctx context.Context, client *mautrix.Client, roomID id.RoomID, eventID id.EventID, newBody string) (id.EventID, error) {
|
||||
if client == nil {
|
||||
return "", fmt.Errorf("matrix_message_send: client no puede ser nil")
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: "* " + newBody,
|
||||
NewContent: &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: newBody,
|
||||
},
|
||||
RelatesTo: (&event.RelatesTo{}).SetReplace(eventID),
|
||||
}
|
||||
return matrixSendEvent(ctx, client, roomID, event.EventMessage, content)
|
||||
}
|
||||
|
||||
// MatrixSendReaction envía un evento m.reaction con m.relates_to.rel_type=m.annotation.
|
||||
// key debe ser el emoji unicode raw (ej. "👍"), no shortcode (:thumbsup:).
|
||||
// Las reactions no se cifran aunque el room sea E2EE (comportamiento de mautrix-go).
|
||||
func MatrixSendReaction(ctx context.Context, client *mautrix.Client, roomID id.RoomID, targetEventID id.EventID, key string) (id.EventID, error) {
|
||||
if client == nil {
|
||||
return "", fmt.Errorf("matrix_message_send: client no puede ser nil")
|
||||
}
|
||||
content := &event.ReactionEventContent{
|
||||
RelatesTo: event.RelatesTo{
|
||||
Type: event.RelAnnotation,
|
||||
EventID: targetEventID,
|
||||
Key: key,
|
||||
},
|
||||
}
|
||||
return matrixSendEvent(ctx, client, roomID, event.EventReaction, content)
|
||||
}
|
||||
Reference in New Issue
Block a user