Files
fn_registry/functions/infra/matrix/matrix_client_init.go
T
egutierrez 4bce095964 refactor(infra): split de drivers pesados a subpaquetes + fix TestSSEHandler
Mueve duckdb_open, clickhouse_open, postgres_open, matrix_* y keyring_token_store
del paquete monolitico functions/infra a subpaquetes propios
(functions/infra/{duckdb,clickhouse,postgres,matrix,keyring}). El paquete infra ya
no importa los drivers (go-duckdb, clickhouse-go, pgx, mautrix, go-keyring), por lo
que las apps que solo usan funciones ligeras (process, cron, http, sqlite) dejan de
arrastrarlos. Reduccion de binarios: dag_engine 72->10MB, registry_api 70->8.7MB,
services_api 70->9MB, call_monitor 68->6.6MB, sqlite_api 70->8.9MB.

Los IDs del registry se mantienen estables (domain: infra en frontmatter). Se
preservan los build tags goolm/libolm de matrix_crypto_init.

Tambien corrige TestSSEHandler: el test leia el body con un unico Read() que con
HTTP chunked solo capturaba el primer evento; ahora usa io.ReadAll hasta EOF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:48:59 +02:00

154 lines
5.5 KiB
Go

package matrix
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
)
// MatrixClientInitConfig parametriza la inicializacion del cliente Matrix.
type MatrixClientInitConfig struct {
// HomeserverURL es la URL base del servidor Matrix (Synapse/Dendrite/etc.).
// Ejemplo: "https://matrix-af2f3d.organic-machine.com"
HomeserverURL string
// UserID es el MXID del usuario. Formato "@local:servidor".
// Ejemplo: "@egutierrez:matrix-af2f3d.organic-machine.com"
UserID string
// AccessToken es el Bearer token obtenido del flow OIDC (mas_oidc_loopback).
// No puede estar vacio.
AccessToken string
// DeviceID del cliente Matrix. Si vacio, se descubre via /whoami al inicializar.
// Recomendado guardarlo en keyring tras el primer uso para evitar la llamada extra.
DeviceID string
// StoreDir es el directorio donde se persiste el estado de sync (next_batch, filter_id).
// Se crea con permisos 0700 si no existe. Puede ser relativo (se convierte a absoluto).
// Ejemplo: "~/.matrix_client_pc/egutierrez/" (no expandido automaticamente — usar os.UserHomeDir).
StoreDir string
// EnableCrypto activa el crypto store SQLite para Olm/Megolm (E2EE).
// En v0.1.0 devuelve error — la implementacion completa esta en issue 0150.
EnableCrypto bool
}
// MatrixClientInitResult contiene el cliente listo y los paths de persistencia.
type MatrixClientInitResult struct {
// Client es el *mautrix.Client listo para Sync/SendMessage.
// UserID, AccessToken y DeviceID ya estan configurados.
Client *mautrix.Client
// StorePath es la ruta al directorio de persistencia de sync state.
StorePath string
// CryptoPath es la ruta calculada para el crypto store SQLite.
// Vacio si EnableCrypto=false. En v0.1.0 siempre vacio (no implementado).
CryptoPath string
}
// MatrixClientInit construye un *mautrix.Client listo para hacer Sync,
// sin manejar el login (que ya hizo el flow OIDC via mas_oidc_loopback).
//
// Pasos:
// 1. Valida inputs (HomeserverURL parseable, UserID formato "@x:server", AccessToken no vacio).
// 2. Crea StoreDir con permisos 0700.
// 3. Llama mautrix.NewClient con las credenciales.
// 4. Si DeviceID esta vacio, hace Whoami para descubrirlo (sum latency ~100ms).
// 5. Si EnableCrypto=true, devuelve error (issue 0150 lo implementa).
// 6. Devuelve MatrixClientInitResult con el cliente configurado.
func MatrixClientInit(cfg MatrixClientInitConfig) (*MatrixClientInitResult, error) {
// 1. Validar HomeserverURL
if cfg.HomeserverURL == "" {
return nil, fmt.Errorf("matrix_client_init: HomeserverURL no puede estar vacio")
}
if _, err := url.ParseRequestURI(cfg.HomeserverURL); err != nil {
return nil, fmt.Errorf("matrix_client_init: HomeserverURL invalido %q: %w", cfg.HomeserverURL, err)
}
if !strings.HasPrefix(cfg.HomeserverURL, "http://") && !strings.HasPrefix(cfg.HomeserverURL, "https://") {
return nil, fmt.Errorf("matrix_client_init: HomeserverURL debe empezar con http:// o https:// (got %q)", cfg.HomeserverURL)
}
// Validar UserID: debe ser "@local:servidor"
if cfg.UserID == "" {
return nil, fmt.Errorf("matrix_client_init: UserID no puede estar vacio")
}
if !strings.HasPrefix(cfg.UserID, "@") || !strings.Contains(cfg.UserID, ":") {
return nil, fmt.Errorf("matrix_client_init: UserID invalido %q — formato esperado @local:servidor", cfg.UserID)
}
// Validar AccessToken
if cfg.AccessToken == "" {
return nil, fmt.Errorf("matrix_client_init: AccessToken no puede estar vacio")
}
// Validar StoreDir
if cfg.StoreDir == "" {
return nil, fmt.Errorf("matrix_client_init: StoreDir no puede estar vacio")
}
// En v0.1.0 crypto no esta implementado
if cfg.EnableCrypto {
return nil, fmt.Errorf("matrix_client_init: crypto not implemented in v0.1.0, see issue 0150")
}
// Convertir StoreDir a absoluto si es relativo
storeDir := cfg.StoreDir
if !filepath.IsAbs(storeDir) {
abs, err := filepath.Abs(storeDir)
if err != nil {
return nil, fmt.Errorf("matrix_client_init: no se pudo resolver StoreDir %q: %w", storeDir, err)
}
storeDir = abs
}
// 2. Crear StoreDir con permisos 0700 (datos sensibles)
if err := os.MkdirAll(storeDir, 0700); err != nil {
return nil, fmt.Errorf("matrix_client_init: no se pudo crear StoreDir %q: %w", storeDir, err)
}
// 3. Construir cliente mautrix
client, err := mautrix.NewClient(cfg.HomeserverURL, id.UserID(cfg.UserID), cfg.AccessToken)
if err != nil {
return nil, fmt.Errorf("matrix_client_init: mautrix.NewClient failed: %w", err)
}
// 4. DeviceID: usar el proporcionado o descubrir via Whoami
if cfg.DeviceID != "" {
client.DeviceID = id.DeviceID(cfg.DeviceID)
} else {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
whoami, err := client.Whoami(ctx)
if err != nil {
// Distinguir token invalido (M_UNKNOWN_TOKEN) de error de red
if errors.Is(err, mautrix.MUnknownToken) {
return nil, fmt.Errorf("matrix_client_init: access token invalido o expirado (M_UNKNOWN_TOKEN) — refrescar via OIDC: %w", err)
}
return nil, fmt.Errorf("matrix_client_init: Whoami failed (servidor caido o token invalido): %w", err)
}
client.DeviceID = whoami.DeviceID
}
// Calcular CryptoPath (aunque no se use en v0.1.0)
cryptoPath := ""
// CryptoPath calculado pero no inicializado en v0.1.0
_ = filepath.Join(storeDir, "crypto.db") // reservado para matrix_crypto_init_go_infra (issue 0150)
return &MatrixClientInitResult{
Client: client,
StorePath: storeDir,
CryptoPath: cryptoPath,
}, nil
}