c441366f89
- mas_oidc_loopback_go_infra: OAuth2 PKCE + loopback HTTP for desktop login - keyring_token_store_go_infra: persist OAuth tokens in SO keychain - matrix_client_init_go_infra: init mautrix.Client from access_token + whoami Plus go.work workspace including matrix_client_pc sub-repo for shared import path during dev. All 3 fns tagged matrix-mas capability group. Tests: TestMasOidcLoopback (15 cases), TestKeyringTokenStore (5 cases, SKIP on headless), TestMatrixClientInit (6 cases) — all green/skip. Refs: dev/issues/0147-matrix-client-pc-scaffold.md Refs: dataforge/matrix_client_pc commit f28c2b1
154 lines
5.5 KiB
Go
154 lines
5.5 KiB
Go
package infra
|
|
|
|
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
|
|
}
|