# Matrix Client-Server Protocol — referencia para matrix_client_pc

Documentacion del trafico real entre `matrix_client_pc` y el homeserver (Synapse + MAS).
Foco: que endpoint se llama, con que payload, que vuelve, y como mapea al tipo Go bound a Wails (frontend Mantine).

**Fuentes:**
- Matrix Client-Server API v1.11 — https://spec.matrix.org/v1.11/client-server-api/
- MSC3861 (OAuth2 + MAS) — https://github.com/matrix-org/matrix-spec-proposals/pull/3861
- mautrix-go v0.23 — capa que consumimos (no llamamos endpoints crudos en Go salvo casos puntuales)
- element-web (`sources/element-web/`) — referencia de comportamiento UI; usa `matrix-js-sdk` que ataca los mismos endpoints

**Convencion:**
- Base homeserver: `https://matrix.organic-machine.com`
- Base MAS: `https://mas.organic-machine.com`
- Todas las requests autenticadas llevan `Authorization: Bearer <access_token>` salvo login.
- Todas las respuestas son JSON UTF-8.

---

## Indice

1. [Flujo de login OIDC (MAS)](#1-flujo-de-login-oidc-mas)
2. [Bootstrap de sesion](#2-bootstrap-de-sesion)
3. [Sync loop](#3-sync-loop)
4. [Listado de rooms + state](#4-listado-de-rooms--state)
5. [Timeline (messages history)](#5-timeline-messages-history)
6. [Envio de mensajes](#6-envio-de-mensajes)
7. [E2EE (Olm/Megolm + cross-signing)](#7-e2ee-olmmegolm--cross-signing)
8. [Media (mxc://)](#8-media-mxc)
9. [Profile + presence](#9-profile--presence)
10. [Mapeo Go (bound) ↔ TS (frontend)](#10-mapeo-go-bound--ts-frontend)
11. [Errores: matriz canonica](#11-errores-matriz-canonica)

---

## 1. Flujo de login OIDC (MAS)

Cubierto por `mas_oidc_loopback_go_infra` (registry). Flow OAuth2 Authorization Code + PKCE.

### 1.1 Discovery del issuer

```
GET https://matrix.organic-machine.com/.well-known/matrix/client
```

Respuesta:
```json
{
  "m.homeserver": { "base_url": "https://matrix.organic-machine.com" },
  "org.matrix.msc2965.authentication": {
    "issuer": "https://mas.organic-machine.com/",
    "account": "https://mas.organic-machine.com/account"
  },
  "org.matrix.msc4143.rtc_foci": [ { "type": "livekit", "livekit_service_url": "..." } ]
}
```

`org.matrix.msc2965.authentication.issuer` indica que el HS delega auth a MAS.

### 1.2 OIDC discovery

```
GET https://mas.organic-machine.com/.well-known/openid-configuration
```

Respuesta (claves consumidas):
```json
{
  "issuer": "https://mas.organic-machine.com/",
  "authorization_endpoint": "https://mas.organic-machine.com/authorize",
  "token_endpoint": "https://mas.organic-machine.com/oauth2/token",
  "registration_endpoint": "https://mas.organic-machine.com/oauth2/registration",
  "scopes_supported": ["openid", "urn:matrix:org.matrix.msc2967.client:api:*", "urn:matrix:org.matrix.msc2967.client:device:*"],
  "code_challenge_methods_supported": ["S256"]
}
```

### 1.3 Dynamic client registration (RFC 7591)

```
POST https://mas.organic-machine.com/oauth2/registration
Content-Type: application/json

{
  "client_name": "matrix_client_pc",
  "application_type": "native",
  "redirect_uris": ["http://127.0.0.1:<loopback_port>/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:<device_id>"
}
```

Respuesta:
```json
{ "client_id": "01HEXXXX...", "client_id_issued_at": 1716565000 }
```

`device_id` lo genera el cliente (16 chars hex). Se reutiliza en cada login para preservar las Olm sessions.

### 1.4 Authorization request (browser)

mautrix-go abre browser SO con:
```
GET https://mas.organic-machine.com/authorize?
    response_type=code&
    client_id=<client_id>&
    redirect_uri=http://127.0.0.1:<port>/callback&
    scope=openid+urn:matrix:org.matrix.msc2967.client:api:*+urn:matrix:org.matrix.msc2967.client:device:<device_id>&
    state=<random>&
    code_challenge=<base64url(sha256(verifier))>&
    code_challenge_method=S256
```

Usuario aprueba en MAS UI. MAS redirige a:
```
http://127.0.0.1:<port>/callback?code=<auth_code>&state=<state>
```

### 1.5 Token exchange

```
POST https://mas.organic-machine.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<auth_code>&
redirect_uri=http://127.0.0.1:<port>/callback&
client_id=<client_id>&
code_verifier=<verifier>
```

Respuesta:
```json
{
  "access_token": "mat_xxx...",
  "token_type": "Bearer",
  "expires_in": 300,
  "refresh_token": "mar_xxx...",
  "scope": "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:<device_id>"
}
```

Persistido por `keyring_token_store_go_infra`.

### 1.6 Refresh

`expires_in` corto (5 min). mautrix refresca automaticamente con:
```
POST https://mas.organic-machine.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=<rt>&
client_id=<client_id>
```

Misma respuesta. Si MAS rota refresh_token (`rotate_refresh_tokens: true`), el viejo deja de servir tras un grace period.

### 1.7 Logout

```
POST https://mas.organic-machine.com/oauth2/revoke
Content-Type: application/x-www-form-urlencoded

token=<refresh_token>&
token_type_hint=refresh_token&
client_id=<client_id>
```

Mas Logout local: borrar token de keyring + `client.Logout()` de mautrix (envia `POST /_matrix/client/v3/logout`).

---

## 2. Bootstrap de sesion

Tras obtener token, el cliente arranca con `matrix_client_init_go_infra`.

### 2.1 /whoami — validar token + descubrir DeviceID

```
GET /_matrix/client/v3/account/whoami
Authorization: Bearer <access_token>
```

Respuesta:
```json
{
  "user_id": "@lucas:organic-machine.com",
  "device_id": "QWERTYUIOP",
  "is_guest": false
}
```

Usado para rellenar `SessionView` (`UserID`, `DeviceID`).

### 2.2 /versions — capacidades del HS (opcional)

```
GET /_matrix/client/versions
```

Respuesta:
```json
{
  "versions": ["v1.1", "v1.2", ..., "v1.11"],
  "unstable_features": {
    "org.matrix.msc3861": true,
    "org.matrix.msc4108": true
  }
}
```

Util si la app condiciona features por capability.

---

## 3. Sync loop

Cubierto por `matrix_sync_service_go_infra`. Long-poll continuo.

### 3.1 Initial sync

```
GET /_matrix/client/v3/sync?
    filter=<filter_id_o_json>&
    timeout=0
Authorization: Bearer <access_token>
```

Respuesta (esquema simplificado):
```json
{
  "next_batch": "s12345_67_8_9_10_11_12_13",
  "rooms": {
    "join": {
      "!roomId:server": {
        "summary": { "m.heroes": ["@bob:server"], "m.joined_member_count": 2, "m.invited_member_count": 0 },
        "state": { "events": [ <state events> ] },
        "timeline": {
          "events": [ <message events> ],
          "limited": true,
          "prev_batch": "t12-34_5_6"
        },
        "ephemeral": { "events": [ <typing, receipts> ] },
        "account_data": { "events": [ <m.tag, etc> ] },
        "unread_notifications": { "highlight_count": 0, "notification_count": 1 }
      }
    },
    "invite": { "!roomId:server": { "invite_state": { "events": [...] } } },
    "leave": { ... }
  },
  "presence": { "events": [...] },
  "account_data": { "events": [ <m.direct, m.push_rules, etc> ] },
  "to_device": { "events": [ <m.room.encrypted, m.room_key, etc> ] },
  "device_lists": { "changed": ["@bob:server"], "left": [] },
  "device_one_time_keys_count": { "signed_curve25519": 50 }
}
```

### 3.2 Incremental sync (long-poll)

```
GET /_matrix/client/v3/sync?since=<next_batch>&timeout=30000
```

mautrix re-llama con el `next_batch` previo. timeout 30s. Si nada cambia → respuesta vacia tras 30s. Si hay evento nuevo → respuesta inmediata.

### 3.3 Eventos clave que matrix_client_pc procesa

| Event type | Donde aparece | Que hacer |
|---|---|---|
| `m.room.message` | `rooms.join.X.timeline.events` | Render en timeline |
| `m.room.encrypted` | `rooms.join.X.timeline.events` | Descifrar via Crypto.Decrypt → re-render como `m.room.message` |
| `m.room.member` | state + timeline | Update memberlist |
| `m.room.name` / `m.room.topic` / `m.room.avatar` | state | Update room header |
| `m.room.encryption` | state | Marcar room como E2EE |
| `m.room.redaction` | timeline | Tachar evento referido |
| `m.reaction` (m.annotation) | timeline | Agregar al evento target |
| `m.typing` (ephemeral) | `rooms.join.X.ephemeral` | Indicador "X is typing" |
| `m.receipt` (ephemeral) | `rooms.join.X.ephemeral` | Read markers de otros users |
| `m.direct` (account_data global) | `account_data.events` | Marcar rooms como DM |
| `m.tag` (account_data por room) | `rooms.join.X.account_data` | Favoritos / low priority |
| `m.room_key` (to_device) | `to_device.events` | Inbox group session — guardar para descifrar mensajes futuros |

### 3.4 Filters

Para acotar el sync (perf). Pre-creado con:
```
POST /_matrix/client/v3/user/<user_id>/filter
Authorization: Bearer <token>

{
  "room": {
    "timeline": { "limit": 50 },
    "state": { "lazy_load_members": true }
  },
  "presence": { "not_types": ["m.presence"] }
}
```

Respuesta: `{"filter_id": "1"}`. Se pasa como `?filter=1` en /sync. mautrix lo gestiona internamente.

---

## 4. Listado de rooms + state

Cubierto por `matrix_room_list_go_infra`. Lee del store del SyncStore (no requiere request extra si el sync ya corrio).

Si se quiere fetch directo:

### 4.1 Joined rooms

```
GET /_matrix/client/v3/joined_rooms
```

Respuesta:
```json
{ "joined_rooms": ["!a:server", "!b:server", ...] }
```

### 4.2 Room state (todo el estado actual)

```
GET /_matrix/client/v3/rooms/<roomId>/state
```

Respuesta: array de state events:
```json
[
  { "type": "m.room.name", "state_key": "", "content": { "name": "Equipo" }, "sender": "@admin:server", ... },
  { "type": "m.room.topic", "state_key": "", "content": { "topic": "..." }, ... },
  { "type": "m.room.encryption", "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" }, ... },
  { "type": "m.room.member", "state_key": "@lucas:server", "content": { "membership": "join", "displayname": "Lucas" }, ... }
]
```

### 4.3 State event puntual

```
GET /_matrix/client/v3/rooms/<roomId>/state/<eventType>[/<stateKey>]
```

Ejemplo nombre:
```
GET /_matrix/client/v3/rooms/!a:server/state/m.room.name
```

Respuesta: `content` del evento directo:
```json
{ "name": "Equipo" }
```

### 4.4 Mapeo a `infra.RoomSummary`

| Campo Go | Origen |
|---|---|
| `RoomID` | key del map `rooms.join` |
| `Name` | `m.room.name` state event → `content.name`. Fallback: `summary.m.heroes` + display names |
| `CanonicalAlias` | `m.room.canonical_alias` state event → `content.alias` |
| `AvatarMxc` | `m.room.avatar` → `content.url` |
| `Topic` | `m.room.topic` → `content.topic` |
| `IsDirect` | `account_data` global `m.direct` lista al roomID |
| `IsSpace` | `m.room.create` → `content.type == "m.space"` |
| `IsEncrypted` | existencia de state event `m.room.encryption` |
| `Tags` | `rooms.join.X.account_data` event `m.tag` → keys del map |
| `LastEventTs` | `timeline.events[-1].origin_server_ts` |

---

## 5. Timeline (messages history)

Cubierto por `MatrixService.LoadTimeline` → mautrix `client.Messages(...)`.

### 5.1 /messages — paginacion historica

```
GET /_matrix/client/v3/rooms/<roomId>/messages?
    from=<token>&            # prev_batch del sync o de respuesta anterior
    dir=b&                   # b=backward (mas antiguo), f=forward
    limit=50&
    filter=<json_opcional>
Authorization: Bearer <token>
```

Respuesta:
```json
{
  "start": "<token_from>",
  "end": "<token_para_siguiente_pagina>",
  "chunk": [
    { "type": "m.room.message", "event_id": "$abc", "sender": "@x:s", "origin_server_ts": 1716..., "content": { "msgtype": "m.text", "body": "hola" } },
    ...
  ],
  "state": [ <state events relevantes en este rango> ]
}
```

### 5.2 /context — un evento + vecinos

Para ir a un permalink:
```
GET /_matrix/client/v3/rooms/<roomId>/context/<eventId>?limit=10
```

Respuesta:
```json
{
  "event": { ... },
  "events_before": [ ... ],
  "events_after": [ ... ],
  "start": "<token>",
  "end": "<token>",
  "state": [ ... ]
}
```

### 5.3 Mapeo a `MatrixEvent`

| Campo Go | Origen |
|---|---|
| `EventID` | `event_id` |
| `RoomID` | path param (no esta en el body de /messages) |
| `Sender` | `sender` |
| `Type` | `type` |
| `Ts` | `origin_server_ts` |
| `Body` | `content.body` (si `type=m.room.message`) |
| `EncryptedRaw` | `true` si `type=m.room.encrypted` y no se pudo descifrar |

---

## 6. Envio de mensajes

Cubierto por `matrix_message_send_go_infra` → `MatrixService.SendText` / `SendMarkdown`.

### 6.1 PUT /send/{type}/{txnId}

```
PUT /_matrix/client/v3/rooms/<roomId>/send/<eventType>/<txnId>
Authorization: Bearer <token>
Content-Type: application/json

<content>
```

`txnId` = string unico por request (mautrix usa nanosecond timestamp). Idempotente: re-PUT del mismo txnId NO duplica.

Respuesta:
```json
{ "event_id": "$xyz..." }
```

### 6.2 Variantes de `content`

**Texto plano:**
```json
{ "msgtype": "m.text", "body": "hola mundo" }
```

**Markdown con HTML formateado:**
```json
{
  "msgtype": "m.text",
  "body": "hola **mundo**",
  "format": "org.matrix.custom.html",
  "formatted_body": "hola <strong>mundo</strong>"
}
```

**Reply (MSC2781 fallback + m.in_reply_to):**
```json
{
  "msgtype": "m.text",
  "body": "> <@alice:s> hola\n\nrespuesta",
  "format": "org.matrix.custom.html",
  "formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!r:s/$abc\">In reply to</a> <a href=\"https://matrix.to/#/@alice:s\">@alice:s</a><br>hola</blockquote></mx-reply>respuesta",
  "m.relates_to": { "m.in_reply_to": { "event_id": "$abc" } }
}
```

**Edit (m.replace):**
```json
{
  "msgtype": "m.text",
  "body": "* nuevo body",
  "m.new_content": { "msgtype": "m.text", "body": "nuevo body" },
  "m.relates_to": { "rel_type": "m.replace", "event_id": "$abc" }
}
```

**Reaction (m.annotation):** type=`m.reaction`, no `m.room.message`:
```json
{
  "m.relates_to": { "rel_type": "m.annotation", "event_id": "$abc", "key": "👍" }
}
```

PUT a `/send/m.reaction/<txnId>`.

### 6.3 En rooms E2EE

Si `client.Crypto != nil` (configurado via `matrix_crypto_init_go_infra`), mautrix:
1. Toma el `content` plano.
2. Lo cifra con la outbound Megolm session del room.
3. Sustituye el body por:
```json
{
  "algorithm": "m.megolm.v1.aes-sha2",
  "ciphertext": "AwgBE...",
  "session_id": "abc...",
  "sender_key": "curve25519...",
  "device_id": "QWERTYUIOP"
}
```
4. PUT a `/send/m.room.encrypted/<txnId>` (tipo cambia a `m.room.encrypted`).

El frontend NO ve esta diferencia — `SendText` retorna el `event_id` igual.

### 6.4 Redact (delete event)

```
PUT /_matrix/client/v3/rooms/<roomId>/redact/<eventId>/<txnId>
{ "reason": "spam" }
```

Respuesta: `{ "event_id": "$redact..." }`.

---

## 7. E2EE (Olm/Megolm + cross-signing)

Cubierto por `matrix_crypto_init_go_infra` + mautrix cryptohelper.

### 7.1 Upload device keys + one-time keys

```
POST /_matrix/client/v3/keys/upload
{
  "device_keys": {
    "user_id": "@lucas:s",
    "device_id": "QWERTYUIOP",
    "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
    "keys": {
      "curve25519:QWERTYUIOP": "<base64>",
      "ed25519:QWERTYUIOP": "<base64>"
    },
    "signatures": { "@lucas:s": { "ed25519:QWERTYUIOP": "<sig>" } }
  },
  "one_time_keys": {
    "signed_curve25519:AAAAAA": { "key": "<base64>", "signatures": { ... } },
    ...
  }
}
```

Respuesta:
```json
{ "one_time_key_counts": { "signed_curve25519": 50 } }
```

mautrix mantiene siempre ≥50 OTKs disponibles. Cuando /sync devuelve `device_one_time_keys_count.signed_curve25519 < 50`, sube mas.

### 7.2 Query device keys (de otros)

```
POST /_matrix/client/v3/keys/query
{ "device_keys": { "@bob:s": [] } }     # array vacio = todos los devices
```

Respuesta:
```json
{
  "device_keys": {
    "@bob:s": {
      "DEVICE1": { "user_id": "@bob:s", "device_id": "DEVICE1", "algorithms": [...], "keys": {...}, "signatures": {...} }
    }
  },
  "master_keys": { "@bob:s": { ... cross-signing master key ... } },
  "self_signing_keys": { ... },
  "user_signing_keys": { ... }
}
```

### 7.3 Claim OTK (para iniciar Olm session)

```
POST /_matrix/client/v3/keys/claim
{ "one_time_keys": { "@bob:s": { "DEVICE1": "signed_curve25519" } } }
```

Respuesta:
```json
{
  "one_time_keys": {
    "@bob:s": { "DEVICE1": { "signed_curve25519:AAAA": { "key": "...", "signatures": {...} } } }
  }
}
```

### 7.4 Send-to-device (compartir Megolm room key)

```
PUT /_matrix/client/v3/sendToDevice/m.room.encrypted/<txnId>
{
  "messages": {
    "@bob:s": {
      "DEVICE1": {
        "algorithm": "m.olm.v1.curve25519-aes-sha2",
        "ciphertext": { "<bob_curve25519>": { "type": 0|1, "body": "..." } },
        "sender_key": "<lucas_curve25519>"
      }
    }
  }
}
```

Una vez Bob descifra ese to-device con Olm, obtiene el `m.room_key` con la inbound Megolm session que le permite descifrar todos los mensajes posteriores del room.

### 7.5 Cross-signing keys (4S, secret storage)

```
POST /_matrix/client/v3/keys/device_signing/upload
POST /_matrix/client/v3/keys/signatures/upload
GET  /_matrix/client/v3/user/<user_id>/account_data/m.cross_signing.{master,self_signing,user_signing}
GET  /_matrix/client/v3/user/<user_id>/account_data/m.secret_storage.default_key
GET  /_matrix/client/v3/user/<user_id>/account_data/m.secret_storage.key.<keyId>
```

mautrix cryptohelper gestiona todo. matrix_client_pc solo expone API "verify device" / "recover from passphrase" — ambos traducen a estas requests.

---

## 8. Media (mxc://)

### 8.1 Download

```
GET /_matrix/client/v1/media/download/<serverName>/<mediaId>
Authorization: Bearer <token>            # v1 require auth (v3 sin auth deprecado)
```

Respuesta: bytes binarios. `Content-Type` indica el mime.

### 8.2 Thumbnail

```
GET /_matrix/client/v1/media/thumbnail/<serverName>/<mediaId>?
    width=96&height=96&method=scale&animated=false
```

### 8.3 Upload

```
POST /_matrix/media/v3/upload
Content-Type: image/png
Authorization: Bearer <token>

<binary bytes>
```

Respuesta:
```json
{ "content_uri": "mxc://organic-machine.com/abc123def" }
```

### 8.4 mxc:// → URL http

Helper canonico (no es endpoint, es transformacion):
```
mxc://organic-machine.com/abc123def
  → /_matrix/client/v1/media/download/organic-machine.com/abc123def
```

Para avatares en `AvatarMxc` del `RoomSummary`, el frontend resuelve con un wrapper que appende el Bearer token. NO se puede meter en `<img src>` directo (necesita auth header).

---

## 9. Profile + presence

### 9.1 Displayname + avatar

```
GET /_matrix/client/v3/profile/<userId>
```

Respuesta:
```json
{ "displayname": "Lucas", "avatar_url": "mxc://..." }
```

### 9.2 Set displayname

```
PUT /_matrix/client/v3/profile/<userId>/displayname
{ "displayname": "Nuevo nombre" }
```

### 9.3 Presence

```
PUT /_matrix/client/v3/presence/<userId>/status
{ "presence": "online", "status_msg": "trabajando" }
```

Synapse desactiva presence por defecto. matrix_client_pc no debe asumir que esta disponible.

---

## 10. Mapeo Go (bound) ↔ TS (frontend)

Wails serializa structs Go → objetos JS via JSON tags. Mantener sincronia con tipos TS.

### 10.1 SessionView

```go
type SessionView struct {
    UserID        string `json:"user_id"`
    DeviceID      string `json:"device_id"`
    HomeserverURL string `json:"homeserver_url"`
    HasToken      bool   `json:"has_token"`
    ExpiresAt     string `json:"expires_at,omitempty"`
}
```

```ts
export interface SessionView {
  user_id: string;
  device_id: string;
  homeserver_url: string;
  has_token: boolean;
  expires_at?: string;
}
```

### 10.2 RoomSummary

```go
type RoomSummary struct {
    RoomID         string   `json:"room_id"`
    Name           string   `json:"name,omitempty"`
    CanonicalAlias string   `json:"canonical_alias,omitempty"`
    AvatarMxc      string   `json:"avatar_mxc,omitempty"`
    Topic          string   `json:"topic,omitempty"`
    IsDirect       bool     `json:"is_direct"`
    IsSpace        bool     `json:"is_space"`
    IsEncrypted    bool     `json:"is_encrypted"`
    Tags           []string `json:"tags,omitempty"`
    LastEventTs    int64    `json:"last_event_ts,omitempty"`
}
```

```ts
export interface RoomSummary {
  room_id: string;
  name?: string;
  canonical_alias?: string;
  avatar_mxc?: string;
  topic?: string;
  is_direct: boolean;
  is_space: boolean;
  is_encrypted: boolean;
  tags?: string[];
  last_event_ts?: number;
}
```

### 10.3 MatrixEvent (historico timeline)

```go
type MatrixEvent struct {
    EventID      string `json:"event_id"`
    RoomID       string `json:"room_id"`
    Sender       string `json:"sender"`
    Type         string `json:"type"`
    Ts           int64  `json:"ts"`
    Body         string `json:"body,omitempty"`
    EncryptedRaw bool   `json:"encrypted_raw"`
}
```

```ts
export interface MatrixEvent {
  event_id: string;
  room_id: string;
  sender: string;
  type: string;          // "m.room.message" | "m.room.encrypted" | "m.reaction" | ...
  ts: number;            // ms desde epoch
  body?: string;         // plain text (puede ser fallback del m.room.encrypted descifrado)
  encrypted_raw: boolean; // true = no se pudo descifrar, mostrar placeholder
}
```

### 10.4 SyncEventView (push en vivo via Wails event)

```go
type SyncEventView struct {
    Type    string `json:"type"`
    RoomID  string `json:"room_id"`
    EventID string `json:"event_id"`
    Sender  string `json:"sender"`
    Ts      int64  `json:"ts"`
    Body    string `json:"body,omitempty"`
}
```

Emit Wails: `runtime.EventsEmit(ctx, "matrix:event", view)`.

Listener TS:
```ts
EventsOn("matrix:event", (ev: SyncEventView) => { ... });
```

### 10.5 Diagnostics

```go
type Diagnostics struct {
    SyncRunning      bool   `json:"sync_running"`
    LastSyncAt       string `json:"last_sync_at,omitempty"`
    CryptoEnabled    bool   `json:"crypto_enabled"`
    HomeserverURL    string `json:"homeserver_url"`
    NextBatchToken   string `json:"next_batch_token,omitempty"`
    PendingEvents    int    `json:"pending_events"`
}
```

---

## 11. Errores: matriz canonica

Synapse devuelve `application/json` con shape estandar Matrix:

```json
{
  "errcode": "M_FORBIDDEN",
  "error": "You do not have permission to send a message in this room"
}
```

HTTP status varia. Tabla minima:

| HTTP | errcode | Significado | Accion en matrix_client_pc |
|---|---|---|---|
| 401 | `M_UNKNOWN_TOKEN` | Token revocado / expirado | Trigger refresh OIDC; si falla → forzar re-login |
| 401 | `M_MISSING_TOKEN` | Sin Authorization | Bug — siempre debe ir Bearer |
| 403 | `M_FORBIDDEN` | Permission denied | Mostrar toast, no retry |
| 403 | `M_USER_DEACTIVATED` | Cuenta deshabilitada | Logout + mensaje claro |
| 404 | `M_NOT_FOUND` | Room/event no existe | Refrescar lista |
| 429 | `M_LIMIT_EXCEEDED` | Rate limit | Backoff exponencial (mautrix lo hace solo) |
| 400 | `M_INVALID_PARAM` | Body malformado | Bug del cliente |
| 400 | `M_UNKNOWN` | Generico | Log + reportar |
| 502/503/504 | (no JSON) | HS caido | Reintentar con backoff |

**Errores E2EE (descifrado):**

| Sintoma | Causa | Mitigacion |
|---|---|---|
| `m.room.encrypted` recibido pero no se descifra | Falta `m.room_key` inbound (llego antes de unirnos) | Mostrar placeholder; key backup recovery (si configurado) |
| `Olm: OUTBOUND_GROUP_SESSION_NOT_FOUND` al enviar | Crypto store corrupto | Wipe + re-bootstrap (ver memoria `agents-e2ee-unblock-pattern`) |
| `M_FORBIDDEN` al PUT /send/m.room.encrypted | El user no esta en room o no tiene power level | Bug de UI; chequear membership antes |

---

## Apendice A — Endpoints que matrix_client_pc consume HOY (auditoria)

| MatrixService method | mautrix call | Endpoint HTTP |
|---|---|---|
| `Login()` | `mas_oidc_loopback.Run()` | OIDC flow (§1) |
| `Start(userID)` | `client.SyncWithContext()` | GET /sync (§3) |
| `StartNoCrypto(userID)` | igual sin crypto store | GET /sync (§3) |
| `ListRooms()` | lee del SyncStore en memoria | (ninguno extra) |
| `LoadTimeline(roomID, limit)` | `client.Messages(roomID, "", "b", filter, limit)` | GET /rooms/{id}/messages (§5.1) |
| `SendText(roomID, body)` | `client.SendMessageEvent(roomID, "m.room.message", content)` | PUT /rooms/{id}/send/m.room.message/{txn} (§6.1) |
| `SendMarkdown(roomID, md)` | parse markdown → mismo SendMessageEvent | PUT /rooms/{id}/send/m.room.message/{txn} (§6.1) |
| `Logout(userID)` | `client.Logout()` + revoke MAS | POST /_matrix/client/v3/logout + POST /oauth2/revoke |

## Apendice B — Endpoints pendientes (gap del cliente)

Estos NO estan envueltos todavia y son el roadmap natural:

| Capability | Endpoint | Funcion registry candidata |
|---|---|---|
| Read receipts | `POST /rooms/{id}/receipt/m.read/{eventId}` | `matrix_receipt_send_go_infra` |
| Typing notifications | `PUT /rooms/{id}/typing/{userId}` | `matrix_typing_set_go_infra` |
| Redact (delete) | `PUT /rooms/{id}/redact/{eventId}/{txn}` | `matrix_redact_event_go_infra` |
| Reply / edit / reaction | ya cubierto por `matrix_message_send` (variantes) | — (anadir wrappers en MatrixService) |
| Upload media | `POST /_matrix/media/v3/upload` | `matrix_media_upload_go_infra` |
| Download mxc | `GET /_matrix/client/v1/media/download/...` | `matrix_media_download_go_infra` |
| Resolve room alias | `GET /_matrix/client/v3/directory/room/{alias}` | `matrix_resolve_alias_go_infra` |
| Join room | `POST /_matrix/client/v3/join/{idOrAlias}` | `matrix_room_join_go_infra` |
| Leave room | `POST /_matrix/client/v3/rooms/{id}/leave` | `matrix_room_leave_go_infra` |
| Invite | `POST /_matrix/client/v3/rooms/{id}/invite` | `matrix_room_invite_go_infra` |
| Create room | `POST /_matrix/client/v3/createRoom` | `matrix_room_create_go_infra` |
| Set displayname | `PUT /profile/{userId}/displayname` | `matrix_profile_set_go_infra` |
| Verify device (SAS) | flujo to-device `m.key.verification.*` | `matrix_verify_sas_go_infra` |
| Key backup (recovery) | `GET/PUT /room_keys/...` + 4S | `matrix_key_backup_go_infra` |

Cada gap → issue + spawn `fn-constructor`.

---

## Apendice C — Referencias element-web (solo lectura, AGPL — NO copiar codigo)

| Tema | Fichero element-web | Concepto a portar |
|---|---|---|
| Reply fallback parse | `apps/web/src/utils/Reply.ts` | Stripping `<mx-reply>` antes de render |
| Composer markdown | `apps/web/src/editor/serialize.ts` | Pipeline markdown → html sanitizado |
| Permalink parsing | `apps/web/src/utils/permalinks/` | `matrix.to/#/!r:s/$e` → roomId + eventId |
| Reaction aggregation | `apps/web/src/utils/Reactions.ts` | Agrupar por key + contar |
| Room list sort | `apps/web/src/stores/room-list/algorithms/` | Recency / favorites / DM tiers |
| Verification UX state | `apps/web/src/verification.ts` | SAS state machine: started → key_received → confirmed |
| Notifier (push rules) | `apps/web/src/Notifier.ts` | Eval push rules → tray/sound/badge |

Patron: leer fichero element-web como referencia algoritmica, reescribir limpio en Go o TS, citar en `source_file` del frontmatter.

---

## Mantenimiento de este documento

Cada vez que se anade una capability nueva a `MatrixService`:
1. Documentar endpoint(s) reales que mautrix golpea (capturar con `MAUTRIX_LOG_LEVEL=trace` si dudas).
2. Anadir request/response shape al apartado correspondiente.
3. Anadir mapeo Go ↔ TS si es nuevo tipo bound.
4. Mover la fila del Apendice B al Apendice A.

Ultima revision: 2026-05-25.
