> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat

> Le chat sur JMAP : salons, messages, accusés de lecture, rappels, sondages et émojis personnalisés. Plus le transport WebSocket, les indicateurs de saisie, les aperçus de liens et les plafonds de capacité.

Le chat est une extension JMAP d'OxiMail (`urn:oximail:params:jmap:chat`, avec `polls`, `custom-emoji` et `mentions` comme objets de capacité voisins) : le même modèle de requête, les mêmes chaînes d'état et le même push que tous les autres domaines. Un client JMAP obtient le chat avec la mécanique qu'il a déjà. Le temps réel passe par le WebSocket JMAP.

## Objets et méthodes

| Objet                  | Méthodes                                                             | Notes                                                                                           |
| ---------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `ChatRoom`             | `get`, `set`, `query`, `queryChanges`, `changes`                     | Les salons directs et de groupe ; l'appartenance est une liste d'identifiants de Principal.     |
| `ChatMessage`          | `get`, `set`, `query`, `queryChanges`, `changes`                     | Les messages, avec édition (sémantique de patch) et mentions structurelles.                     |
| `ChatReadReceipt`      | `get`, `set`                                                         | L'état de lecture par membre : voir [les accusés de lecture](#les-accusés-de-lecture) plus bas. |
| `ChatPersonalReminder` | `get`, `set`, `query`, `changes`                                     | « Me rappeler ce message » : personnel, invisible pour le salon.                                |
| `CustomEmoji`          | `get`, `set`, `query`, `changes`                                     | Les émojis à l'échelle de l'organisation.                                                       |
| `Poll` / `PollVote`    | `get`, `set`, `query`, `changes` (votes : `set`, `query`, `changes`) | Les sondages en salon.                                                                          |

Les pièces jointes sont des blobs JMAP standard ; le contenu partagé en salon est stocké à l'échelle de l'organisation pour que chaque membre le résolve.

## Transport et vivacité

* **WebSocket** (`/jmap/ws`) : appels de méthode et push sur une seule socket. Les indicateurs de saisie sont des trames WS hors du modèle de requête JMAP, limitées aux **seuls membres du salon** et bridées à une trame par 3 s par (salon, compte) ; les arrêts passent toujours pour que les indicateurs s'effacent.
* **La vivacité est revérifiée.** L'ancre d'authentification de la socket (la session d'appareil) est revalidée périodiquement ; une session révoquée ou expirée ferme en `4401` et le client se reconnecte proprement, au lieu de traîner à moitié mort (le chat coule, les requêtes HTTP échouent). La rotation de routine des tokens ne déconnecte jamais un client sain.

## Plafonds de capacité

L'objet de capacité du chat annonce les quotas appliqués comme données : `{maxRoomsPerAccount: 100, maxParticipantsPerRoom: 200, maxMessageSizeOctets: 65536, maxPinnedPerRoom: 50, maxReadReceiptLiveMembers: 20}`, pour que les clients se bornent avant de rencontrer `overQuota`/`tooLarge` ([multi-tenant](../operator/multi-tenancy)). `maxPinnedPerRoom` plafonne les messages épinglés par salon ; la taille de message borne l'objet de création sérialisé (`content` et `encryptedContent` uniformément) ; `maxReadReceiptLiveMembers` est expliqué sous [les accusés de lecture](#les-accusés-de-lecture).

## Les accusés de lecture

`ChatReadReceipt` est un curseur de lecture par membre, à savoir l'identifiant du dernier message que ce membre a lu. C'est ce qu'un client affiche sous forme d'une pile d'avatars de lecteurs sous un message.

* **Un curseur n'avance jamais que vers l'avant.** `ChatReadReceipt/set` refuse un `lastReadMessageId` explicite plus ancien que celui déjà stocké, avec `invalidProperties`, avant toute écriture : une entrée refusée laisse donc intacts le curseur comme le badge de non-lus. Pour remettre un salon en non-lu, utilisez `destroy` : c'est le sens prévu. Quand le curseur a été *déduit par défaut* au lieu d'être envoyé par l'appelant, ce qui arrive tout seul lorsque le message le plus récent est détruit, ce n'est jamais une erreur : le curseur stocké l'emporte, et la réponse rapporte le curseur que le serveur **détient**, pas le plus ancien qu'on lui a passé.
* **Une lecture prévient les autres membres, en direct.** Marquer un salon comme lu met en file un changement d'état `ChatReadReceipt` pour chacun des *autres* membres, afin que leur surface « vu par » suive la lecture, au lieu d'être juste au chargement puis figée. Deux bornes s'appliquent : un canal qui garde ses curseurs cachés ne prévient personne, et un salon plus grand que `maxReadReceiptLiveMembers` (20 par défaut) ne prévient personne non plus, car une lecture prévient tous les autres membres et le volume croît donc comme le carré de la taille du salon. Au-delà de ce plafond, les curseurs restent exacts et `ChatReadReceipt/get` les renvoie toujours tous : ils se rafraîchissent simplement à la prochaine récupération plutôt que dans la seconde.
* **Les salons directs et de groupe renvoient l'accusé de chaque membre. Un canal non, sauf s'il le choisit.** Le raisonnement derrière la fermeture des curseurs d'un canal, c'est qu'un grand salon de diffusion laisserait fuiter des habitudes de lecture : vrai là, et faux pour le petit canal d'équipe où « qui a vu ce message » est justement la raison de poster. `ChatRoom` porte donc `readReceiptsVisible` : `false` par défaut, pour que la confidentialité reste le défaut et que les salons existants gardent leur comportement, réglable par un **propriétaire** uniquement, et propriété *partagée* du salon plutôt que préférence par membre, puisqu'elle décide de ce que le salon divulgue de ses membres et qu'un membre ne peut pas y répondre pour les autres. La poser sur un salon direct ou de groupe est **refusé** au lieu d'être stocké : ces salons renvoient déjà tous les accusés, et une valeur stockée là serait une promesse que rien ne tient.

## Envoi programmé et aperçus de liens

Deux capacités voisines annoncent des surfaces d'extension de ChatMessage :

* **`urn:oximail:params:jmap:scheduled-send`** : une création `ChatMessage/set` peut porter `scheduledAt` ; le message attend avec un `sendStatus` jusqu'à ce que le planificateur côté serveur le délivre (le même planificateur que les alertes d'agenda).
* **`urn:oximail:params:jmap:link-unfurl`** : le serveur remplit les aperçus de liens sur les messages (récupérés côté serveur derrière une garde anti-SSRF ; les clients ne récupèrent jamais eux-mêmes des URL tierces).

`ChatMessage.unfurls` est un **tableau d'objets typés**, complété au fur et à mesure que chaque URL du corps se résout, et absent tant qu'un message n'a pas d'URL ou qu'aucun aperçu n'a encore abouti :

```json theme={null}
"unfurls": [
  {
    "url": "https://example.org/article",
    "title": "…",
    "description": "…",
    "imageUrl": "https://example.org/og.png",
    "siteName": "Example",
    "ogType": "article",
    "canonicalUrl": "https://example.org/article"
  }
]
```

Tous les champs sauf `url` sont facultatifs et omis quand la page source ne les fournissait pas. Le stockage reste un blob JSON dans une colonne, mais c'est désormais un détail d'implémentation que le fil ne laisse plus filtrer : cette propriété était auparavant projetée telle que la colonne, soit un tableau d'octets bruts (`[91, 123, 34, …]`), ce qui coûtait environ quatre caractères sur le fil par octet utile et obligeait chaque client à réimplémenter octets vers UTF-8 puis analyse JSON, pour une donnée que le serveur produit et comprend entièrement. Le décodage a maintenant lieu une seule fois, côté serveur, là où un blob impossible à analyser échoue bruyamment au lieu d'être silencieusement remplacé par une liste vide.

<Note>
  Le contraste avec `encryptedContent` est volontaire : celui-là est un blob par conception (la trajectoire du chiffrement de bout en bout) et opaque au serveur. `unfurls` ne l'a jamais été.
</Note>

## Comportements à connaître

* **La diffusion est matérialisée** : un message de salon écrit une ligne par membre, ce que les plafonds bornent, et c'est pourquoi les écritures d'appartenance, de message et de journal de changements sont transactionnelles (`/changes` ne peut jamais manquer un message validé).
* **Les mentions sont structurelles** (`@principal` comme donnée, pas un balayage de chaîne) : un renommage ne les casse pas et les clients les rendent fiablement.
* **Les éditions de messages gardent l'historique** ; les références de l'historique d'édition sont suivies par le GC de blobs, une pièce jointe éditée ne devient jamais une référence pendante.
* **Les messages différés et les rappels** passent par le même ordonnanceur côté serveur que les alertes d'agenda.

Les règles de rigueur sont identiques à la [surface courrier](./jmap-mail).
