Lorenzo SanseverinoThe Last Developer
IT EN
Portfolio

Backend

Beebot

Progetto personale di lunga durata: un bot Discord da una parte, un motore statistiche su League of Legends dall’altra.

PRIMO COMMIT
3 febbraio 2022
EVOLUZIONE
Bot Discord → API LoL
DATI
MongoDB + Redis
STACK
Java, Spring, JDA

Origine

Prima le persone nel server. Poi il motore dei dati.

Il primo commit di Beebot è del 3 febbraio 2022. Il punto di partenza è un bot Discord in Java: comandi per il server, riproduzione audio e, poco dopo, caricamento di suoni personalizzati. La storia non comincia con una piattaforma di statistiche già progettata: le funzioni crescono insieme agli usi del bot.

League of Legends diventa progressivamente un dominio a sé. All’inizio ci sono comandi per evocatori, campioni e rune. Nel 2024 arriva il tracking degli LP; quando il tracker comincia a conservare molto più del risultato in classifica, cambia anche nome: da LPTracker a MatchTracker.

Il passaggio importante è dalla consultazione di dati esterni alla costruzione di uno storico proprio. Partite persistite, statistiche derivate e build diventano riutilizzabili da più interfacce. Oggi i comandi Discord e le API HTTP condividono i servizi di dominio; Iodid usa queste API per presentare i dati nel browser.

Come funziona

Dietro una richiesta di profilo.

Gli scenari illustrano il flusso logico tra cache, persistenza e job Riot. I tempi sono dimostrativi: la risposta HTTP distingue dati disponibili, parziali e ancora in preparazione.

beebot / api trace
Scenario
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner HIT · 2 ms
  3. 200 SummonerView 6 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner MISS · 1 ms
  3. MONGO summoner.findOne({ puuid }) HIT · 9 ms
  4. REDIS SET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner 1 ms
  5. 200 SummonerView 15 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner MISS · 1 ms
  3. MONGO summoner.findOne({ puuid }) MISS · 8 ms
  4. QUEUE QueueHandler.immediate(...) → RiotScheduler QUEUED
  5. 202 profile_pending 11 ms
  6. RIOT GET /riot/account/v1/accounts/by-puuid/{puuid} 200 · 141 ms
  7. RIOT GET /lol/summoner/v4/summoners/by-puuid/{puuid} 200 · 152 ms
  8. MONGO summoner.insertOne(profile) 7 ms
  9. REDIS SET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner 1 ms
  10. 200 SummonerView RETRY · 6 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile-by-name/Player/EUW
  2. REDIS Riot ID lookup · Player#EUW · EUW1 MISS · 1 ms
  3. MONGO saved summoner lookup by Riot ID MISS · 8 ms
  4. RIOT GET /riot/account/v1/accounts/by-riot-id/Player/EUW 404 · 138 ms
  5. 404 not_found 149 ms

Esempio con tempi indicativi. Le chiamate a Riot passano sempre dalla coda.

Framework

Un’app Java, quattro strati.

App avvia insieme API Spring, scheduler e bot JDA. I comandi Discord e i controller HTTP non contengono logica di dominio: passano entrambi dagli stessi servizi, che delegano il lavoro lento a code e scheduler.

INGRESSO

JDA per i comandi Discord, Spring su Tomcat embedded per le API.

SERVIZI

Profili, partite, campioni e classifiche, condivisi da bot e API.

JOB

Code e scheduler per chiamate Riot, tracker e calcoli in differita.

DATI

MongoDB canonico, Redis derivato, MariaDB per guild e impostazioni.

Logica

Dall’identità del giocatore alla risposta HTTP.

  1. Un nome per cercare, un PUUID per identificare

    La ricerca accetta un Riot ID o un prefisso e, facoltativamente, uno shard. Il risultato conserva regione e identità del giocatore. Dopo la risoluzione, il PUUID è la chiave stabile per profilo e partite: il nome visibile può cambiare senza diventare una nuova identità.

  2. Servire ciò che esiste, preparare ciò che manca

    I servizi leggono cache Redis e dati persistiti in MongoDB. Se manca ancora l’identità o la base del profilo, l’API restituisce 202. Quando la base c’è ma alcune componenti mancano, può restituire 200 con una vista parziale e metadata.refresh=true. Il client non deve trasformare “in preparazione” in statistiche uguali a zero.

  3. Una richiesta non deve duplicare un lavoro già avviato

    Il registro dei job deduplica il lavoro attivo per tipo, rotta logica e chiave. RiotScheduler separa le chiamate per shard; ComputeScheduler gestisce i calcoli e SyncScheduler coordina le sincronizzazioni. Un lavoro richiesto con priorità maggiore può promuovere quello già in coda senza avviarne una copia.

  4. Lo storico delle partite ha un contratto separato

    La lista delle partite legge i match già persistiti in MongoDB. Filtri, paginazione e faccette non richiedono di attendere Riot durante la GET. Il dettaglio di una partita aggiunge gli eventi della timeline; la lista mantiene partecipanti e ban, ma evita quel carico aggiuntivo.

API ed esempi

API reali, esempi leggibili.

Gli URL usano localhost e identità dimostrative. I frammenti descrivono il contratto del codice: non sono misurazioni o risposte catturate in produzione.

Esempio 01 / bash

Cercare senza confondere nome e regione

Il carattere # fa parte del valore della query e va codificato. Omettendo shard la ricerca è globale; passando EUW1 viene limitata a quella regione. La risposta è una lista, anche quando non ci sono risultati.

curl --get 'http://localhost:8080/api/lol/search' \
  --data-urlencode 'q=Player#EUW' \
  --data-urlencode 'shard=EUW1'

Esempio 02 / http

Caricare il profilo e distinguere gli stati

Il nome e il tag sono segmenti distinti del percorso. La risposta disponibile è un SummonerView, senza un wrapper data. Un 202 indica che la risoluzione o il caricamento della base sono ancora in corso; un 200 può già contenere componenti parziali.

GET /api/lol/EUW1/profile-by-name/Player/EUW?queue=RANKED_SOLO_5X5&season=2026

200 OK       → profilo disponibile, eventualmente parziale
202 Accepted → base del profilo ancora in preparazione
404 Not Found → identità o profilo non trovato

Esempio 03 / bash

Leggere una pagina di partite

Sostituisci example-puuid con il PUUID ottenuto dal profilo. season=previous seleziona la stagione precedente configurata; limit e offset controllano la pagina. Il filtro lane riguarda il giocatore selezionato, non un partecipante qualsiasi della partita.

curl --get 'http://localhost:8080/api/lol/EUW1/profile/example-puuid/matches' \
  --data-urlencode 'queue=RANKED_SOLO_5X5' \
  --data-urlencode 'lane=MID' \
  --data-urlencode 'season=previous' \
  --data-urlencode 'limit=20' \
  --data-urlencode 'offset=0' \
  --data-urlencode 'sort=timeStart:desc'

Timeline

Quattro anni di evoluzioni, non una riscrittura magica.

Le date indicano modifiche documentate in Git. Una modifica o un merge non dimostrano, da soli, la data di un rilascio in produzione.

  1. Il primo bot Java

    Il repository parte con l’app Java e le prime funzioni Discord e audio. A marzo compaiono upload e riproduzione di suoni personalizzati: una delle funzioni che resteranno nel progetto.

  2. Dai comandi testuali agli slash command

    Comincia l’introduzione degli slash command, seguita dalla conversione delle famiglie di comandi. Nel frattempo crescono gestione server, permessi e configurazioni per guild.

  3. League of Legends diventa una funzione riconoscibile

    Il comando champion e i successivi miglioramenti a rune, profili e autocomplete strutturano l’integrazione LoL. Il bot non è più soltanto un insieme di comandi audio.

  4. La prima soundboard

    La cronologia documenta una prima release della soundboard e il lavoro su suoni pubblici e privati. La parte community continua a evolvere accanto alle funzioni di gioco.

  5. Il tracker degli LP

    Arrivano tracking degli LP, comando summoner track e grafico dedicato. Non basta più mostrare uno stato corrente: occorre conservare come quel valore cambia nel tempo.

  6. Da LPTracker a MatchTracker

    Il tracker cambia nome perché raccoglie ormai dati delle partite, non soltanto LP. Seguono ban, oggetti e lavoro di analisi: lo storico diventa la base delle statistiche.

  7. Statistiche dai dati del progetto

    Il comando champion comincia a usare dati raccolti dal sistema. Il significato architetturale è importante: raccogliere, conservare e analizzare sostituisce la sola lettura di una fonte esterna.

  8. Un’interfaccia HTTP per il dominio LoL

    Un ramo dedicato introduce il lavoro su Spring per i dati di League of Legends. Nei mesi successivi i servizi e i contratti diventano un confine riutilizzabile anche fuori da Discord.

  9. Redis entra nel flusso

    La cache Redis viene introdotta e poi estesa a profili, partite, build e dati statici. Le chiavi e le scadenze diventano responsabilità esplicite del sistema.

  10. La migrazione LoL verso MongoDB

    Il merge lol-api-mongo riunisce modelli, persistenza, tracker e documentazione della migrazione. Il dominio LoL passa dai vecchi percorsi SQL a MongoDB; MariaDB mantiene altre responsabilità del bot.

  11. Il lavoro in background diventa osservabile

    Il refactor dei job riorganizza dispatcher, priorità e stato operativo. Il sistema attuale distingue il registro logico dei lavori dalle code e dai worker che li eseguono.

  12. Una cache esterna per i payload Riot

    La cronologia documenta il passaggio a EmptyCacheProvider per R4J e a Redis per le risposte Riot. Cache applicativa, dati canonici e invalidazione hanno confini più espliciti.

  13. Separare senza fingere che sia già tutto distribuito

    L’estrazione dell’artefatto S4B e l’alpha di lol-service esplorano confini indipendenti. Sono tappe della separazione, non una prova che ogni parte del prodotto sia già un servizio autonomo in produzione.

  14. Import dello storico e promozione dei job

    Un nuovo endpoint per lo storico si accompagna al lavoro su MatchService e sulle priorità. La deduplicazione deve restare utile anche quando una richiesta interattiva arriva dopo un import in background.

Stato attuale

Il prodotto attuale è una piattaforma condivisa.

Il punto d’arrivo non è soltanto “un bot con più comandi”. È un nucleo Java che raccoglie e conserva partite, espone contratti HTTP e riusa gli stessi servizi nei comandi Discord. La separazione tra persistenza, cache, calcolo e presentazione permette a Iodid di esistere senza replicare il backend.

Le dimensioni del database e la latenza dipendono da dati e ambiente: questa ricostruzione non attribuisce nuovi numeri di traffico o prestazioni al progetto. Le risposte parziali, il rate limit e il costo di rigenerazione degli aggregati restano vincoli reali del sistema.

Scelte progettuali

Le scelte che reggono il sistema.

Mongo è la fonte, Redis si ricostruisce

Partite, profili e aggregati vivono in MongoDB. Redis conserva cache ricostruibili: componenti del profilo, payload Riot, cataloghi statici e proiezioni.

CostoL’invalidazione non è automatica: va gestita a ogni scrittura.

Le API non aspettano Riot

Un profilo assente risponde 202 e accoda i job. I dati parziali arrivano con 200 e un segnale di refresh.

CostoIl client deve gestire gli stati PENDING, PARTIAL e READY.

Priorità e deduplicazione prima delle copie

Le richieste Riot passano da code per shard. Il registro dei job riusa il lavoro attivo e può promuoverne la priorità quando arriva una richiesta interattiva.

CostoLifecycle, priorità e completamento dei figli devono restare coerenti.

Scope

Incluso

  • Moderazione e gestione server
  • Musica, soundboard, playlist e TTS
  • Profili, partite, build e rune
  • Tracker, classifiche e live game

In corso

  • Separazione in servizi indipendenti
  • Worker Riot e code in moduli dedicati
  • Repository condivisi per job e DTO

Fuori scope

  • Interfaccia web pubblica: vive in Iodid