Messages privés et groupes partagent le même modèle. Une conversation direct a deux membres. Une conversation
group en a jusqu'à GROUP_MAX_MEMBERS. Les parties se lancent depuis une conversation : Parties.
Contrat d'origine : back/GROUPS.md.
| Nom | Défaut | Rôle |
|---|---|---|
GROUP_MAX_MEMBERS |
20 | membres par groupe (créateur compris) |
GROUPS_PER_OWNER |
5 | groupes dont un joueur est owner |
REPORTS_TO_HIDE |
3 (constante) | signalements ouverts qui masquent un message |
GROUP_COLORS |
8 couleurs (constante) | couleur du groupe = GROUP_COLORS[owned % 8] |
Les deux réglages sont dans config.py et exposés par GET /config.
| Table | Clé | Colonnes utiles |
|---|---|---|
conversations |
id |
kind (direct | group), user_a / user_b (privé : les deux ids triés, UNIQUE (user_a, user_b)), name, color_hex, invite_code (UNIQUE, groupe), updated_at (tri de la Boîte) |
conversation_members |
(conversation_id, user_id) |
role (owner | member), joined_at, last_read_at, muted |
messages |
id |
sender_id (NULL = système), kind (text | question | system | match), text, question_id, match_id, hidden, sent_at (indexé, à la microseconde) |
blocks |
(blocker_id, blocked_id) |
voir Sécurité |
reports |
id |
signalements de messages et de joueurs |
direct_conversation(user, other) : cherche par (user_a, user_b) triés, sinon crée la conversation et ses deuxPOST /me/conversations/{pseudo}/messages.
user_or_404(not_me) : 404 si pseudo inconnu, supprimé, ou soi-même.is_blocked_between).POST /me/conversations/{id}/send. Privé : reachable (l'autre existe,GET /me/contacts (joueurs suivis + conversations privéespost_message (services/conversations.py) :
strip) ; ni texte ni question → 422. Texte : 1 000 caractères au plus (OutgoingMessage).question_id : suffixe du fil infini retiré (base_id, -l{n}) ; 404 si la question n'existe pas.kind = "question" si une question est jointe, sinon text.conversation.updated_at = sent_at ; last_read_at de l'auteur = sent_at (écrire vaut lecture).notify_message : push message aux autres membres, sauf ceux qui ont bloqué l'auteur.realtime.conversation_changed (voir Temps réel).Les messages system (création, ajout, départ, renommage, podium) et match (carte) sont écrits par le serveur.
GET /me/conversations/{id}/messages?before=<message_id>&limit= : limit 100 par défaut, 1 à 200. Pagination parsent_at < sent_at(before). Ordre chronologique dans la réponse.visible : hidden = false, et expéditeur absent de mes blocages (les messages système passent toujours).match embarquent match_out (la carte vue par le lecteur).question porte question_id. Sortie : shared_question_out (id, text, kind, thème, couleur deCalcul à la volée dans conversation_out, pour chaque conversation :
COUNT(*) FROM messages
WHERE conversation_id = :c AND hidden = false
AND sender_id IS NOT NULL AND sender_id <> :moi
AND sender_id NOT IN (:mes_blocages)
AND sent_at > :last_read_at -- si last_read_at n'est pas NULL
POST /me/conversations/{id}/read : last_read_at = now. Lancer une partie le fait aussi.add_member pose last_read_at = now : un nouveau membre ne voit pas l'historique en non-lu.GET /me/conversations : trié par updated_at décroissant. Les conversations privées avec un joueur que j'ai| Action | Route | Qui | Contrôles |
|---|---|---|---|
| Créer | POST /groups |
user.subscribed ou is_admin |
403 sinon ; 403 si déjà GROUPS_PER_OWNER groupes en owner |
| Voir | GET /groups/{id} |
membre | 404 sinon |
| Renommer | PATCH /groups/{id} |
tout membre | nom 2 à 40 caractères ; message système |
| Ajouter | POST /groups/{id}/members |
tout membre | 409 déjà membre ; 409 plein ; 403 si blocage entre l'ajouteur et l'ajouté |
| Retirer | DELETE /groups/{id}/members/{pseudo} |
owner |
403 sinon ; 404 si pas membre |
| Quitter | DELETE /groups/{id}/members/{mon pseudo} |
soi-même | — |
| Rejoindre par code | POST /groups/join |
tout joueur connecté | 404 code inconnu ; 409 plein ; 403 si un membre est lié à l'entrant par un blocage (dans un sens ou l'autre, players.blocked_either_way) |
| Nouveau code | POST /groups/{id}/invite-code |
owner |
l'ancien code cesse de marcher |
| Lancer une partie | POST /matches |
tout membre | voir Parties |
| Couper les push | PUT /me/conversations/{id}/mute |
tout membre | pour soi seul |
user.subscribed = is_subscriber et (subscription_expires_at NULL ou futur). Un groupe survit à la fin de
l'abonnement de son créateur.
groups.create)member_pseudos (19 au plus) : @ ajouté, mis en minuscules et dédoublonnés, cherchés sans tenir compte de lalower(pseudo)) ; pseudos inconnus ignorés en silence.GROUP_MAX_MEMBERS − 1.owner, message « X a créé le groupe ».join(by = créateur) : message « X a ajouté Y » et entrée de boîte group.owner. Retirer et régénérer le code lui sont réservés. Tout le reste est ouvert aux membres.owner qui part (ou supprime son compte) : le membre au plus ancien joined_at devient owner. Message « Y gèredelete_conversation efface messages, parties (match_answers, match_players, matches),new_invite_code : 8 caractères tirés avec secrets.choice dans ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (32 symboles,strip().upper(), 4 à 12 caractères acceptés par le schéma.Code : back/app/services/conversations.py (direct_conversation, post_message, notify_message, conversation_out, my_conversations, messages_of, delete_conversation, new_invite_code), back/app/routers/groups.py (create, join, join_by_code, remove, new_code), back/app/routers/me.py (send_message, send_in_conversation, read_conversation, mute_conversation)
inbox_screen.dart, canCreateGroup) l'autoriseisSubscriber || isFounder. Le serveur exige subscribed || is_admin. Un fondateur non abonné et non admin voitowner (sans blocage) peut revenir avec le code tantCOUNT puis un INSERT, sans verrou. Deuxpost_message accepte n'importe quel question_id existant,pending ou rejected.deleted_at (un compte supprimé dont on connaît le@supprime.… peut être ajouté).group. Rien du tout pour l'entrée par code.GET /me/conversations.