Trois outils : le blocage (table blocks), les signalements (tables reports et question_reports), et quelques
plafonds. La modération des questions est décrite dans back/MODERATION.md.
Table blocks : (blocker_id, blocked_id), created_at. Routes (routers/safety.py) :
POST /users/{pseudo}/block, DELETE /users/{pseudo}/block, GET /me/blocks (pseudos bloqués). Idempotent.
Trois helpers :
conversations.blocked_ids(user) : ceux que j'ai bloqués (un sens).conversations.is_blocked_between(a, b) : blocage dans un sens ou l'autre. reachable(conv, user) l'utilise,players.blocked_either_way(user_id) : l'ensemble des joueurs liés à user_id par un blocage, dans un sens ou| Endroit | Contrôle | Effet |
|---|---|---|
POST /me/conversations/{pseudo}/messages |
is_blocked_between |
403 |
POST /me/conversations/{id}/send (privé) |
reachable |
403 |
POST /matches (privé) |
reachable |
403 |
POST /groups (invités) |
is_blocked_between(créateur, invité) |
invité retiré en silence |
POST /groups/{id}/members |
is_blocked_between(ajouteur, ajouté) |
403 |
POST /groups/join (code) |
blocked_either_way(entrant) ∩ membres du groupe |
403 « Impossible de rejoindre ce groupe » si un membre est lié à l'entrant par un blocage |
POST /matches (groupe) |
blocked_either_way(lanceur) |
les membres liés au lanceur par un blocage ne sont ni joueurs ni prévenus (push match) ; les blocages entre deux autres membres ne comptent pas |
POST /users/{pseudo}/follow |
is_blocked_between |
403 « Impossible de suivre ce joueur » |
Recherche (routers/search.py) |
blocked_either_way |
joueurs liés par un blocage absents des résultats |
GET /questions/{id}/comments |
blocked_either_way |
commentaires des joueurs liés par un blocage masqués |
GET /me/conversations |
blocked_ids |
conversation privée masquée (pour le bloqueur) ; dernier message et non-lus sans ses messages |
GET …/messages |
blocked_ids (visible) |
ses messages masqués (cartes de partie comprises) |
Push message |
bloqueurs de l'auteur | pas de push |
| Endroit | Conséquence |
|---|---|
| Envoi dans un groupe | le bloqué écrit toujours ; seul l'affichage chez le bloqueur filtre |
Profil, stats, badges, abonnés (/users/{pseudo}…) |
lisibles |
/ws |
les messages conversation partent à tous les membres, match à tous les joueurs de la partie |
Code : back/app/routers/safety.py (block, unblock, my_blocks), back/app/services/conversations.py (blocked_ids, is_blocked_between, reachable, visible, notify_message), back/app/services/players.py (blocked_either_way), back/app/routers/groups.py (join), back/app/services/matches.py (create_match), back/app/routers/users.py (follow), back/app/routers/search.py, back/app/routers/feed.py (comments)
POST /reports)ContentReportIn : target (message | user), id (id du message, ou pseudo), reason (spam,
harassment, offensive, other), comment (200 caractères).
user_or_404(not_me).uq_reports_open (reporter_id, target, target_id) WHERE resolved_at IS NULL : un seulIntegrityError → 204 silencieux.COUNT(reports ouverts) >= REPORTS_TO_HIDE (3) → messages.hidden = true pour tout le monde.Console (routers/reports_admin.py, préfixe /api/admin/reports) : liste par cible (1 000 ouverts lus au plus),
compteurs, resolve (classer), message/{id}/hidden (masquer ou rétablir, et classer). Côté joueur signalé : la
console affiche blocked_by (nombre de joueurs qui l'ont bloqué). Pas de suspension ni de bannissement de compte.
Seules actions possibles sur un joueur : exclusion des classements, retrait de la photo, droits admin.
POST /questions/{id}/report)ReportIn : wrong_answer, ambiguous, offensive, other.REPORTS_PER_DAY (10) par joueur sur 24 h glissantes → 429.REPORT_MIN_DAYS (3) jours ou moins de 3 jours joués ; sinonmin(1, (confirmés + 1) / (rejetés + 1)).REPORT_THRESHOLD (3,0) → question repassée en pending.Code : back/app/routers/safety.py (report), back/app/routers/reports_admin.py, back/app/routers/feed.py (report), back/app/services/moderation.py (report_weight, requeue_if_reported)
| Limite | Valeur | Mécanisme | Code |
|---|---|---|---|
Connexion (/auth/login) |
5 mauvais mots de passe par pseudo, puis 429 jusqu'à 15 min après le premier échec | LOGIN_MAX_FAILS, LOGIN_LOCK_S, redis.over_limit sur login:fails:<pseudo> |
routers/auth.py (login) |
Inscription par pseudo (/auth/register) |
5 comptes / h par adresse IP, 429 ensuite | REGISTER_PER_IP, redis.over_limit sur register:ip:<ip> ; IP = première de X-Forwarded-For, posée par Caddy (deps.client_ip) |
routers/auth.py (register) |
Codes d'invitation (/groups/join) |
10 codes faux / h par joueur, puis 429 (même le bon code) | JOIN_FAILS_PER_HOUR, redis.over_limit sur join:fails:<user_id> |
routers/groups.py (join_by_code) |
| Liste d'attente du site | 5 / min par IP | redis.over_limit (INCR + EXPIRE) |
routers/waitlist.py |
Push follow |
1 / 24 h par couple | redis.over_limit |
routers/users.py (follow) |
| Signalements de questions | 10 / 24 h par joueur | COUNT SQL |
routers/feed.py (report) |
| Création de questions | 10 / jour (30 avec Slidz+) | QUESTIONS_PER_DAY, QUESTIONS_PER_DAY_PLUS |
voir back/MODERATION.md |
| Groupes | 5 possédés, 20 membres | GROUPS_PER_OWNER, GROUP_MAX_MEMBERS |
routers/groups.py |
| Taille des contenus | message 1 000, commentaire 500, signalement 200, nom de groupe 40 caractères ; photo 8 Mo | schémas Pydantic, avatars.MAX_BYTES |
schemas.py, services/avatars.py |
| Un signalement ouvert par cible | index uniques partiels | PostgreSQL | models.py |
Il n'y a pas de limiteur global (pas de middleware, pas de limite au niveau du proxy dans le code de l'API).
/reports),/ws.target n'accepte que message et user.client_ip prend la première valeur de X-Forwarded-For, fournie par leisFounder côté app, abonné ou admin côté serveur) : voir