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 }