Followers / découverte
Les compteurs num_user_follower / num_user_followed sont maintenus côté MySQL par les triggers t_add_follower et t_delete_follower attachés à user_follow. Hydrogen n’écrit jamais directement dans user_stats ; à la place, après chaque INSERT/DELETE sur user_follow, le service de follow pousse une mise à jour partielle vers l’index Meilisearch users (paramétrable via MEILISEARCH_USERS_INDEX, défaut users_dev) — best-effort, n’annule jamais l’écriture SQL.
Pagination : keyset opaque sur (followed_at, peerId). Paramètres ?cursor=… (page suivante), ?before=… (page précédente), ?limit=<1..100> (défaut 20). La navigation se fait via l’objet links JSON:API (self/first/prev/next) — voir conventions générales. Un curseur malformé renvoie 400.
Ressource users : la ressource utilisateur retournée dans ces listings inclut, en plus des attributs habituels, followersCount, followingCount, mediaCount, albumCount, commentsCount, likesReceivedCount, viewsCount, impressionsCount (lus depuis user_stats). likesReceivedCount est un compteur monotone (jamais décrémenté) du cumul des j'aime reçus sur l’ensemble des médias de l’utilisateur, maintenu par le trigger SQL trg_media_reaction_ai_likes_received. L’attribut followedAt reflète la date de mise en relation (ISO 8601). viewsCount et impressionsCount sont maintenus de manière asynchrone — voir Compteurs de profil (views + impressions).
Flags de relation (viewer authentifié) : quand la requête transporte un Bearer valide, chaque ressource users de ces listings porte trois booléens calculés en deux requêtes batch (une par direction) :
| Flag | Signification |
|---|---|
isFollowedByMe | le viewer suit ce pair |
isFollowingMe | ce pair suit le viewer (réciprocité entrante) |
isMutual | les deux à la fois (dérivé) |
Sur les endpoints publics /users/{userId}/* sans Bearer, ces trois flags sont omis (pas de contexte viewer) — ils ne sont jamais null, simplement absents. Pour interroger la relation avec un seul utilisateur sans paginer une liste, voir GET /api/users/{userId}/relationship.
POST /api/users/me/following/{targetUserId}
Section titled “POST /api/users/me/following/{targetUserId}”Suit la cible.
- Auth : requise (Bearer)
- Action : FollowUserAction
- Path :
{targetUserId}UUID canonique (8-4-4-4-12). - Idempotent : si la relation existe déjà,
200quand même, aucun compteur n’est bumped. - Règles métier :
- Auto-follow interdit (
422follow.selfFollow) - Cible inexistante (
404follow.targetNotFound) - Cible bannie (
403follow.targetBanned) - Acteur non confirmé (
403follow.actorNotConfirmed) - Acteur banni (
403follow.actorBanned) - Rate limit (
429follow.rateLimited) : au-delà deFOLLOW_MAX_PER_WINDOWnouveaux follows (défaut 30) sur une fenêtre glissante deFOLLOW_WINDOW_MINUTESminute(s) (défaut 1), la réponse renvoie429avec l’en-têteRetry-After(secondes) etmeta.retryAfterSeconds. Seuls les follows réellement créés consomment le budget — les re-follows idempotents et les unfollows ne comptent pas.
- Auto-follow interdit (
- Réponse
200 OK:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "userFollows", "id": "<followerUuid>:<targetUuid>", "attributes": { "followerId": "0193f4a2-…", "targetId": "0193f4b0-…" } }}DELETE /api/users/me/following/{targetUserId}
Section titled “DELETE /api/users/me/following/{targetUserId}”Cesse de suivre la cible.
- Auth : requise (Bearer)
- Action : UnfollowUserAction
- Idempotent : appelé sur une relation inexistante, retourne quand même
200(aucun compteur impacté). - Réponse
200 OK: ressourceuserFollowsavecunfollowed: true.
GET /api/users/me/following
Section titled “GET /api/users/me/following”Liste paginée des utilisateurs que l’utilisateur authentifié suit (followed_at DESC).
- Auth : requise (Bearer)
- Action : ListMyFollowingAction
- Query :
?cursor=<opaque>(page suivante),?before=<opaque>(page précédente),?limit=<1..100>(défaut20) - Réponse
200 OK:data[]de ressourcesusers(avecfollowedAt,followersCount,followingCount, et les flags de relationisFollowedByMe = true/isFollowingMe/isMutual), navigation via l’objetlinks.meta:{ "limit": 20, "total": <user_stats.following> }— le compteur exact est lu suruser_stats(maintenu par triggers MySQL), donc gratuit.
GET /api/users/me/followers
Section titled “GET /api/users/me/followers”Liste paginée des utilisateurs qui suivent l’utilisateur authentifié.
- Auth : requise (Bearer)
- Action : ListMyFollowersAction
- Query : idem.
- Réponse
200 OK:data[]de ressourcesusers;isFollowedByMeindique si je suis aussi ce follower (réciprocité),isFollowingMe = truepar définition (ce sont mes followers) etisMutual = isFollowedByMe.meta.total = <user_stats.followers>(compteur dénormalisé).
GET /api/users/{userId}/following
Section titled “GET /api/users/{userId}/following”Liste publique des utilisateurs que {userId} suit.
- Auth : aucune
- Action : ListUserFollowingAction
- Path :
{userId}UUID canonique. - Erreurs :
422si UUID invalide,404si l’utilisateur n’existe pas. - Réponse
200 OK: ressourcesuserssansisFollowedByMe(pas de viewer authentifié).meta.total = <user_stats.following>de l’anchor.
GET /api/users/{userId}/followers
Section titled “GET /api/users/{userId}/followers”Liste publique des utilisateurs qui suivent {userId}.
- Auth : aucune
- Action : ListUserFollowersAction
- Mêmes erreurs / forme de réponse que ci-dessus.
meta.total = <user_stats.followers>de l’anchor.
GET /api/users/{userId}/relationship
Section titled “GET /api/users/{userId}/relationship”Sonde en un seul appel l’état de la relation entre le viewer authentifié et {userId} — évite au client de différ deux listings pour connaître la réciprocité.
- Auth : requise (Bearer) — la réponse est relative au viewer.
- Action : GetUserRelationshipAction
- Path :
{userId}UUID canonique. - Erreurs :
422si UUID invalide,404si l’utilisateur n’existe pas. - Cas particulier : relation avec soi-même →
isSelf = true, tous les flagsfalse(aucune ligneuser_follown’existe), pas de lookup. - Réponse
200 OK:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "relationships", "id": "<viewerUuid>:<targetUuid>", "attributes": { "targetId": "0193f4b0-…", "isFollowedByMe": true, "isFollowingMe": false, "isMutual": false, "isSelf": false } }}GET /api/users/search
Section titled “GET /api/users/search”Recherche / découverte d’utilisateurs via l’index Meilisearch (MEILISEARCH_USERS_INDEX).
- Auth : aucune (si la requête transporte malgré tout un Bearer valide,
isFollowedByMeest calculé pour le viewer). - Action : SearchUsersAction
- Query :
?q=<texte>: full-text optionnel surusername/nickname. Vide = pur browse.?sort=popular→ tri parstats.num_user_follower:desc?sort=recent→ tri parjoined_at:desc- sinon : pertinence Meilisearch.
?limit=<1..100>(défaut20),?offset=<int>(défaut0).
- Pipeline : Meilisearch retourne des
id(UUID hex), Hydrogen ré-hydrate les entitésUseretUserStatsdepuis MySQL pour garantir une formeusersstrictement identique aux autres endpoints (avatar/cover résolus,isBanned/isConfirmedà jour…). - Réponse
200 OK:
{ "jsonapi": { "version": "1.1" }, "data": [ { "type": "users", "id": "…", "attributes": { … } } ], "links": { "self": "https://api.example/api/users/search?limit=20&q=alice", "first": "https://api.example/api/users/search?limit=20&q=alice", "prev": null, "next": "https://api.example/api/users/search?limit=20&offset=20&q=alice", "last": "https://api.example/api/users/search?limit=20&offset=40&q=alice" }, "meta": { "totalHits": 42, "limit": 20, "offset": 0, "query": "alice" }}- Pagination : offset-based (Meilisearch). Navigation via
links.{self,first,prev,next,last}—links.lastest disponible ici (contrairement aux endpoints keyset) carmeta.totalHitspermet de le calculer. 503: si Meilisearch est indisponible/inconnu (l’erreur API est propagée danserrors[].detail).