10 sept. 2026

GameSentry

Une application Laravel et React qui suit les parties de League of Legends et Teamfight Tactics pour publier des résumés et des commentaires générés par IA sur Discord.

Couverture de GameSentry montrant des résumés de parties de League of Legends et Teamfight Tactics

Le projet

GameSentry suit les parties de joueurs de League of Legends et de Teamfight Tactics, puis publie un résumé dans un salon Discord[1]. Le message rassemble les statistiques de la partie et un court commentaire humoristique en français, généré à partir de ces données.

J’ai développé ce projet personnel autour d’un usage simple: retrouver les résultats des joueurs directement dans l’espace où l’on échange déjà avec eux. Derrière ce fonctionnement, le principal travail concerne la coordination de plusieurs services externes et la gestion des traitements en arrière-plan.

Ce que permet l’application

Depuis l’interface web, un utilisateur peut associer un serveur Discord[1], sélectionner le salon de destination et ajouter des joueurs à suivre avec leur Riot ID[7]. Chaque joueur est également associé à un utilisateur Discord[1] pour permettre sa mention dans les notifications.

Les résumés s’adaptent au jeu: résultat et statistiques individuelles pour League of Legends, placement et composition pour Teamfight Tactics. Le tableau de bord permet de consulter les notifications récentes et l’utilisation du compte.

Le projet intègre également des offres Free et Pro avec des limites de serveurs, de joueurs et de générations quotidiennes. La gestion des abonnements s’appuie sur Stripe[2] et Laravel Cashier[3]. Une interface d’administration donne accès aux états opérationnels et à la relance des traitements en échec.

Pourquoi Laravel ?

Le backend utilise Laravel 13[4] avec PHP 8.4[6]. Le besoin central de GameSentry est d’exécuter régulièrement des traitements qui dépendent de plusieurs services externes. Laravel[4] rassemble les briques nécessaires dans un même environnement: scheduler, queues, client HTTP, cache, verrous et accès à la base avec Eloquent[5].

Le scheduler sélectionne les joueurs dont le suivi est arrivé à échéance et programme le travail. Les queues exécutent les appels à l’API Riot[7], la génération du commentaire et l’envoi Discord[1] en arrière-plan, avec des tentatives, des délais de reprise et des timeouts. Le tableau de bord reste indépendant de ces traitements, même si certaines opérations de configuration, comme la résolution d’un Riot ID[6], restent synchrones.

Cette organisation permet de conserver les règles métier dans une seule application PHP[5], tout en séparant les processus web, worker et scheduler en production. Elle évite d’ajouter un service indépendant pour chaque intégration.

Fortify pour l’authentification

Laravel Fortify[8] fournit les mécanismes d’authentification utilisés par l’application: inscription, connexion, récupération du mot de passe, vérification de l’adresse e-mail, double authentification et passkeys. Les écrans sont réalisés avec React[9] et Inertia[10] pendant que Fortify[8] porte les parcours côté serveur.

S’appuyer sur cette bibliothèque réduit le code d’authentification spécifique à maintenir. Les autorisations métier restent toutefois propres à GameSentry: Fortify[8] ne décide pas quels serveurs Discord[1] ou joueurs un utilisateur peut gérer.

Cashier pour les abonnements

Laravel Cashier[3] relie les utilisateurs et leurs abonnements Stripe[2] au modèle Laravel[4]. Il est utilisé pour ouvrir une session Checkout, accéder au portail de facturation et suivre l’état de l’abonnement. Les webhooks Stripe[2] alimentent la synchronisation du plan de l’utilisateur: le retour du navigateur après paiement ne suffit pas à lui seul à établir ses droits.

Les limites applicatives restent centralisées dans un service distinct, PlanLimitService. Cashier[3] gère la relation avec Stripe[2] et GameSentry détermine ce que les offres Free et Pro autorisent.

Pourquoi Inertia et React ?

L’interface utilise React 19[9], TypeScript[11] et Tailwind CSS 4[12], reliés à Laravel[4] par Inertia 3[10]. Les contrôleurs Laravel[4] renvoient les pages et leurs données et Inertia[10] prend en charge les navigations côté client et React[9] affiche les composants interactifs.

Pour ce tableau de bord, cette architecture permet de garder les routes, la session et les autorisations dans Laravel[4], sans construire une API REST dédiée à un frontend séparé. Elle conserve une expérience proche d’une SPA, avec un backend qui reste responsable de la navigation et des données transmises.

Une SPA React[9] découplée aurait demandé un contrat d’API et davantage de coordination autour des formulaires, des erreurs et de l’authentification. Next.js[13] aurait été pertinent pour une application centrée sur son écosystème ou des besoins de rendu spécifiques. Associé au backend Laravel[4] existant, il aurait ajouté un second framework applicatif à exploiter. GameSentry n’a pas besoin de cette séparation pour son interface principalement authentifiée.

Le compromis est assumé: les pages React[9] sont liées aux contrôleurs Laravel[4]. Si un client mobile ou une API publique devenait nécessaire, il faudrait concevoir cette interface supplémentaire. Inertia[10] ne la fournit pas automatiquement.

Architecture de l’application

Le navigateur dialogue avec Laravel[4] à travers Caddy[14]. Le scheduler et le worker partagent PostgreSQL[15] avec le processus web et la file de jobs est elle-même stockée en base. Le schéma présente les principaux flux applicatifs, sans détailler les réseaux Docker[16].

Architecture de GameSentryLe navigateur accède à Laravel par Caddy. Laravel et le scheduler écrivent dans PostgreSQL. Le worker consomme les jobs et enregistre leurs résultats dans la même base.NavigateurReact + InertiaCaddyEntrée HTTP / HTTPSLaravel webFortify · CashierPostgreSQLDonnées · jobs · cache · sessionsSchedulerPlanifie les jobs en baseWorkerTraite les jobs et appelle les API
Une même image applicative pour trois processus: web, scheduler et worker. Les appels externes sont détaillés dans la section suivante.

Les intégrations externes

Discord: installer le bot et publier les messages

L’association d’un serveur passe par le parcours d’autorisation OAuth2 de Discord[1], avec les scopes d’installation du bot. Un paramètre state conservé en session est vérifié au retour. Le backend vérifie ensuite la présence du bot et récupère les salons proposés à l’utilisateur.

Ce parcours sert à installer et associer le bot et la connexion au compte GameSentry reste gérée par Laravel[4] et Fortify[8]. Les requêtes vers Discord[1] utilisent le token du bot conservé côté serveur. Les messages combinent une mention du joueur, le commentaire généré et un embed de statistiques. Les mentions autorisées sont limitées à l’utilisateur Discord[1] ciblé.

Riot: identifier les joueurs et détecter les nouvelles parties

L’API Account permet de résoudre un Riot ID[6] en PUUID, l’identifiant utilisé pour retrouver l’historique. Les services de GameSentry prennent ensuite en charge les endpoints et les routages propres à League of Legends et Teamfight Tactics, ainsi que les métadonnées de profil nécessaires.

L’application interroge périodiquement les historiques: elle ne reçoit pas un événement push pour chaque fin de partie. Les API Riot[6] appliquent plusieurs niveaux de limites, notamment par application, méthode et service. Le budget de jobs configuré dans GameSentry est donc un réglage interne, pas une reproduction exacte du quota Riot[6].

Les réponses 429 déclenchent une temporisation par jeu, calculée en tenant compte de Retry-After lorsqu’il est disponible. Le volume de joueurs influence aussi l’intervalle de polling. Ces protections limitent la pression sur les API, sans promettre un suivi instantané ni l’absence de rate limiting.

Data Dragon: enrichir les résumés visuels

Data Dragon[17] fournit les ressources statiques de Riot[6] utilisées pour les icônes de profil et les illustrations de champions League of Legends. GameSentry construit les URL correspondant aux ressources et à leur version pendant que les données des parties proviennent, elles, des API de matchs.

La configuration de ces visuels est séparée du traitement principal. Une ressource non disponible ne doit pas empêcher la construction du résumé: les images peuvent être omises.

Groq: commenter des statistiques déjà calculées

Le service Groq[18] reçoit un résumé normalisé de la partie. Le calcul et la sélection des statistiques restent dans le code applicatif et le modèle intervient pour produire un court commentaire humoristique en français.

Le prompt demande une phrase complète, concise et fondée sur les données fournies. Cette consigne ne constitue pas une garantie absolue sur la réponse d’un modèle. Les appels disposent de timeouts et d’un quota quotidien. Le commentaire obtenu est enregistré pour être réutilisé si l’envoi Discord[1] doit être retenté.

Dans l’implémentation actuelle, une erreur de génération entraîne une reprise du job. Si le quota quotidien est épuisé, la notification passe en échec: il n’existe pas encore de mode dégradé envoyant uniquement les statistiques.

Pourquoi une offre Pro ?

Chaque joueur suivi ajoute des consultations périodiques de Riot[6], puis potentiellement une récupération de partie, une génération et un envoi Discord[1]. Les quotas par compte rendent cette consommation plus prévisible et évitent de proposer un usage illimité sur une infrastructure et des API à capacité limitée.

Limite par compteFreePro
Serveurs Discord[1] associés13
Joueurs suivis au total315
Appels Groq[18] par jour50250

L’offre Pro donne accès à une capacité supérieure tout en conservant des bornes explicites. Elle permet également de mettre en œuvre un parcours complet d’abonnement, de facturation et de synchronisation des droits.

L’abonnement ne relève pas les quotas attribués par Riot[6]. Les deux offres partagent les mêmes mécanismes de régulation. Limiter les comptes aide à maîtriser la charge. Les budgets de polling, les temporisations et la gestion des erreurs restent nécessaires quel que soit le plan.

Séparer la découverte des parties de leur notification

Le traitement est divisé en deux étapes. Un premier job interroge l’historique Riot[6], compare les identifiants reçus au dernier identifiant connu et crée les notifications correspondant aux nouvelles parties. Un second récupère les détails, prépare le résumé, sollicite Groq[18] et envoie le message à Discord[1].

Cette séparation permet de reprendre l’envoi d’une notification sans refaire systématiquement toute la découverte des parties. Lors de l’ajout d’un joueur, l’application initialise un point de départ dans son historique: ses anciennes parties ne sont pas annoncées comme nouvelles.

Le polling tient compte du nombre de joueurs et de budgets configurés. Les réponses de limitation de Riot[6] déclenchent une temporisation par jeu. La notification reste donc périodique, avec un délai dépendant du scheduler, de la file et des API externes.

De la découverte à la notificationLe scheduler lance le polling. Les limites Riot provoquent une temporisation. Les nouvelles parties créent des notifications persistantes. Un second job prépare le message, le conserve et le transmet à Discord.SchedulerSélection des joueurs à interrogerJob de pollingHistorique Riot + curseurRéponse 429Cooldown par jeu et repriseNouvelles notificationsUne ligne par joueur et partieJob de notificationDétails Riot → résumé → GroqPersistance puis envoiMessage + nonce → DiscordÉtat finalEnvoyé ou échec visible
Sans nouvelle partie, aucune notification n'est créée. Les erreurs peuvent provoquer une reprise. Le statut envoyé est enregistré après la livraison.

Préparer les reprises et limiter les doublons

Une requête externe peut réussir alors que le worker s’arrête avant d’enregistrer son résultat. Relancer aveuglément le job risquerait alors de republier le même message.

GameSentry combine plusieurs protections: contrainte d’unicité entre joueur et partie en base, mécanismes d’unicité des jobs, verrous pendant leur exécution et nonce conservé pour l’envoi Discord[1]. Le résumé, le commentaire généré et les données du message sont enregistrés avant l’envoi afin de pouvoir être réutilisés lors d’une reprise.

Ces mécanismes réduisent les doublons dans les scénarios de reprise prévus, ils ne constituent pas une promesse de livraison exactement une fois dans toutes les situations. Les erreurs et les notifications échouées restent visibles pour permettre leur diagnostic.

Du développement au déploiement

Le développement local utilise SQLite[19]. En production, PostgreSQL 17[15] stocke les données applicatives, mais aussi la file de jobs, le cache et les sessions. L’application n’a ainsi pas besoin d’un service Redis[20] supplémentaire.

Docker Compose[16] sépare les processus web, worker et scheduler, qui utilisent la même image applicative. GitHub Actions[21] exécute les validations avant sa publication sur GHCR[22]. Le déploiement sélectionne l’image du commit concerné, résout sa référence par digest, applique les migrations et vérifie les services ainsi que l’endpoint public de santé. Un verrou partagé sur le VPS sérialise les déploiements et les opérations de sauvegarde afin qu’ils ne se chevauchent pas.

Le dépôt comprend des tests PHPUnit[23], de l’analyse statique PHP[5] et des contrôles TypeScript[11]. La CI exerce également les tests sur PostgreSQL[15], en complément de SQLite[19].

Bilan et pistes d’amélioration

L’intérêt de GameSentry dépasse la génération du commentaire: le projet m’a permis de travailler sur une chaîne asynchrone reliant plusieurs API, avec des quotas, des états persistants et des reprises après erreur.

Les prochaines améliorations concernent notamment les tests directs du pipeline de polling et de livraison, les métriques opérationnelles et les préférences de notification. Le périmètre actuel couvre League of Legends et Teamfight Tactics. Valorant reste une piste conditionnée à l’accès API nécessaire. Il serait également intéressant de proposer un mode dégradé lorsque le quota Groq[18] est épuisé, en envoyant uniquement les statistiques.

Références techniques

Les numéros dans le texte renvoient aux documentations officielles ci-dessous.

  1. Discord: Installation OAuth2 et API du bot.
  2. Stripe Billing: Abonnements, paiement et facturation.
  3. Laravel Cashier: Intégration de Stripe dans Laravel.
  4. Laravel: Queues, reprises et traitements en arrière-plan. Voir aussi le scheduler.
  5. Eloquent: ORM et modèles de données.
  6. PHP: Manuel du langage.
  7. Riot Games Developer Portal: Limites des API : application, méthode et service.
  8. Laravel Fortify: Authentification côté serveur.
  9. React: Composants et interfaces interactives.
  10. Inertia: Navigation et partage de données entre le backend et les pages.
  11. TypeScript: Typage du code frontend.
  12. Tailwind CSS: Styles utilitaires.
  13. Next.js: Framework évoqué comme alternative, non utilisé dans le projet.
  14. Caddy: Serveur HTTP et HTTPS.
  15. PostgreSQL 17: Base de données de production.
  16. Docker Compose: Exécution des services conteneurisés.
  17. Data Dragon: Ressources statiques de League of Legends.
  18. Groq: API de génération de texte.
  19. SQLite: Base de données locale.
  20. Redis: Service évoqué comme alternative, non requis ici.
  21. GitHub Actions: Automatisation des validations et du déploiement.
  22. GitHub Container Registry: Publication et récupération des images de conteneurs.
  23. PHPUnit: Tests automatisés PHP.