Files
egutierrez 41bafa57cc chore: auto-commit (17 archivos)
- app.md
- applog.go
- frontend/package.json
- frontend/package.json.md5
- frontend/vite.config.ts
- go.mod
- main.go
- matrix_service.go
- sqlite_driver.go
- .wails_dev.log
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:16 +02:00

928 lines
27 KiB
Markdown

# 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.