36a485ea26
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>
122 lines
4.8 KiB
Go
122 lines
4.8 KiB
Go
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)
|
|
}
|