41bafa57cc
- 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>
928 lines
27 KiB
Markdown
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.
|