Skip to content

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) :

FlagSignification
isFollowedByMele viewer suit ce pair
isFollowingMece pair suit le viewer (réciprocité entrante)
isMutualles 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à, 200 quand même, aucun compteur n’est bumped.
  • Règles métier :
    • Auto-follow interdit (422 follow.selfFollow)
    • Cible inexistante (404 follow.targetNotFound)
    • Cible bannie (403 follow.targetBanned)
    • Acteur non confirmé (403 follow.actorNotConfirmed)
    • Acteur banni (403 follow.actorBanned)
    • Rate limit (429 follow.rateLimited) : au-delà de FOLLOW_MAX_PER_WINDOW nouveaux follows (défaut 30) sur une fenêtre glissante de FOLLOW_WINDOW_MINUTES minute(s) (défaut 1), la réponse renvoie 429 avec l’en-tête Retry-After (secondes) et meta.retryAfterSeconds. Seuls les follows réellement créés consomment le budget — les re-follows idempotents et les unfollows ne comptent pas.
  • 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 : ressource userFollows avec unfollowed: true.

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éfaut 20)
  • Réponse 200 OK : data[] de ressources users (avec followedAt, followersCount, followingCount, et les flags de relation isFollowedByMe = true / isFollowingMe / isMutual), navigation via l’objet links. meta : { "limit": 20, "total": <user_stats.following> } — le compteur exact est lu sur user_stats (maintenu par triggers MySQL), donc gratuit.

Liste paginée des utilisateurs qui suivent l’utilisateur authentifié.

  • Auth : requise (Bearer)
  • Action : ListMyFollowersAction
  • Query : idem.
  • Réponse 200 OK : data[] de ressources users ; isFollowedByMe indique si je suis aussi ce follower (réciprocité), isFollowingMe = true par définition (ce sont mes followers) et isMutual = isFollowedByMe. meta.total = <user_stats.followers> (compteur dénormalisé).

Liste publique des utilisateurs que {userId} suit.

  • Auth : aucune
  • Action : ListUserFollowingAction
  • Path : {userId} UUID canonique.
  • Erreurs : 422 si UUID invalide, 404 si l’utilisateur n’existe pas.
  • Réponse 200 OK : ressources users sans isFollowedByMe (pas de viewer authentifié). meta.total = <user_stats.following> de l’anchor.

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.

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 : 422 si UUID invalide, 404 si l’utilisateur n’existe pas.
  • Cas particulier : relation avec soi-même → isSelf = true, tous les flags false (aucune ligne user_follow n’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
}
}
}

Recherche / découverte d’utilisateurs via l’index Meilisearch (MEILISEARCH_USERS_INDEX).

  • Auth : aucune (si la requête transporte malgré tout un Bearer valide, isFollowedByMe est calculé pour le viewer).
  • Action : SearchUsersAction
  • Query :
    • ?q=<texte> : full-text optionnel sur username / nickname. Vide = pur browse.
    • ?sort=popular → tri par stats.num_user_follower:desc
    • ?sort=recent → tri par joined_at:desc
    • sinon : pertinence Meilisearch.
    • ?limit=<1..100> (défaut 20), ?offset=<int> (défaut 0).
  • Pipeline : Meilisearch retourne des id (UUID hex), Hydrogen ré-hydrate les entités User et UserStats depuis MySQL pour garantir une forme users strictement 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.last est disponible ici (contrairement aux endpoints keyset) car meta.totalHits permet de le calculer.
  • 503 : si Meilisearch est indisponible/inconnu (l’erreur API est propagée dans errors[].detail).