Files
fn_registry/functions/infra/matrix_message_send.go
T
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00

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)
}