# Tutorial bot Telegram per film

> Questo tutorial costruisce un bot Telegram con grammY su Node.js. Invia /movie fight club e il bot risponde con locandina, anno, durata e valutazione da amodb. Legge entrambi i token da variabili d'ambiente, ritenta su 429 con Retry-After, mette in cache le risposte in memoria e si ferma su una chiave rifiutata. Richiede circa 20 minuti.

## Cosa costruisci

Costruisci un bot Telegram che risponde a `/movie fight club` con locandina, titolo, anno, durata e valutazione. Ti servono Node.js 20.6 o successivo, un token del bot ottenuto da BotFather dentro Telegram e una chiave amodb. Il bot usa grammY e il `fetch` integrato, quindi basta un solo pacchetto npm. Calcola circa 20 minuti.

## Passaggio 1: ottieni due credenziali

Scegli un piano nella pagina dei [prezzi](https://amodb.dev/it/pricing/). Il piano stabilisce quante richieste al minuto può inviare il tuo bot. Crea una chiave nella dashboard del tuo account, lunga 32 caratteri esadecimali. Poi apri una chat con BotFather in Telegram, invia `/newbot`, rispondi alle due domande e copia il token che stampa.

Metti entrambi i valori in un file `.env` e aggiungi quel file a `.gitignore`:

```bash
BOT_TOKEN=YOUR_BOT_TOKEN
AMODB_KEY=YOUR_API_KEY
```

## Passaggio 2: crea il progetto

```bash
mkdir movie-bot && cd movie-bot
npm init -y
npm pkg set type=module
npm install grammy
```

L'impostazione `type` attiva la sintassi `import` e l'`await` di primo livello. La pagina [autenticazione](https://amodb.dev/it/authentication/) mostra le forme della chiave come header e come `api_key`.

## Passaggio 3: rispondi al primo comando

grammY mette il testo che segue un comando in `ctx.match`. Inizia con una funzione di aiuto che invia l'header Bearer e un comando che cerca.

```js
import { Bot } from 'grammy';

const bot = new Bot(process.env.BOT_TOKEN);

async function getJson(path, params) {
  const url = new URL('https://api.amodb.dev/3' + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` } });
  return res.json();
}

bot.command('movie', async (ctx) => {
  const found = await getJson('/search/movie', { query: ctx.match, language: 'en-US' });
  await ctx.reply(found.results[0]?.title ?? 'No movie found.');
});

bot.start();
```

## Passaggio 4: costruisci la risposta con una locandina

Prendi il primo risultato della ricerca, caricalo con `append_to_response=ratings` e costruisci una didascalia. L'URL della locandina è `https://img.amodb.dev/t/p/w500` più `poster_path`. Telegram scarica la foto da solo, e `img.amodb.dev` non richiede chiave. Una didascalia contiene al massimo 1024 caratteri, quindi il codice taglia la trama.

```js
function buildCaption(movie) {
  const year = (movie.release_date || '').slice(0, 4) || 'year unknown';
  const imdb = movie.ratings?.imdb;
  const rating = imdb
    ? `IMDb ${imdb.value}`
    : movie.vote_average ? `TMDB ${movie.vote_average.toFixed(1)}` : 'not known';
  const lines = [`${movie.title} (${year})`, `Rating: ${rating}`];
  if (movie.runtime) lines.push(`Runtime: ${movie.runtime} min`);
  if (movie.overview) lines.push('', movie.overview.slice(0, 600));
  return lines.join('\n').slice(0, 1024);
}
```

Ogni fonte in `ratings` è un oggetto con `value` e `votes`, oppure `null`. Quando `imdb` è `null`, la didascalia ripiega sul `vote_average` aggregato. La pagina [valutazioni](https://amodb.dev/it/ratings/) spiega tutte e cinque le fonti.

## Passaggio 5: gestisci errori e limiti di richieste

Lascia che i gestori sollevino eccezioni e intercetta tutto in un unico punto. Senza `bot.catch`, grammY ferma il bot quando un gestore solleva un'eccezione.

| Caso                     | Causa                                       | Reazione del bot                                                                                                           |
| ------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 401                      | Chiave mancante, sbagliata o sospesa        | La registra nel log, avvisa la chat, esce. Ogni comando fallirebbe.                                                        |
| 429                      | Oltre le tue richieste al minuto            | Attende i secondi di `Retry-After`, con un massimo di 30, ritenta due volte, poi chiede all'utente di riprovare più tardi. |
| `results` vuoto          | Nessun titolo corrisponde alla ricerca      | Risponde che non è stato trovato alcun film.                                                                               |
| Timeout o errore di rete | `fetch` solleva un'eccezione                | Risponde con un breve messaggio di scuse e registra l'errore.                                                              |
| Locandina rifiutata      | Telegram non riesce a recuperare l'immagine | Invia la didascalia come testo.                                                                                            |

Il ciclo qui sotto sta dentro `getJson`. `AmodbError` è una piccola sottoclasse di `Error` con un campo `status`, mostrata nel listato finale.

```js
for (let attempt = 0; ; attempt++) {
  const res = await fetch(id, { headers, signal: AbortSignal.timeout(10_000) });
  if (res.status === 429 && attempt < 2) {
    const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 30);
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
    continue;
  }
  if (!res.ok) throw new AmodbError(res.status);
  return res.json();
}
```

Ogni comando costa due richieste, quindi il piano da 20 richieste al minuto serve circa 10 nuove ricerche al minuto. Vedi [limiti di richieste](https://amodb.dev/it/rate-limits/) ed [errori](https://amodb.dev/it/errors/) per i dettagli.

## Passaggio 6: metti le risposte in cache

Il controllo del limite di richieste viene prima della cache edge, quindi solo una cache nel tuo bot risparmia richieste. Tieni una `Map` dall'URL ai dati e un'ora di scadenza. Usa i tempi dell'edge: 10 minuti per la ricerca e 6 ore per i dettagli. Un tetto di dimensione impedisce a un bot sempre attivo di crescere senza limite.

Anche le righe della cache stanno dentro `getJson`.

```js
const hit = cache.get(id);
if (hit && hit.expires > Date.now()) return hit.data;
// ...after a successful fetch:
if (cache.size > 500) cache.delete(cache.keys().next().value);
cache.set(id, { data, expires: Date.now() + ttlMs });
```

## Passaggio 7: il bot completo

Salvalo come `bot.js`.

```js
import { Bot } from 'grammy';

const { BOT_TOKEN, AMODB_KEY } = process.env;
if (!BOT_TOKEN || !AMODB_KEY) {
  console.error('Set BOT_TOKEN and AMODB_KEY first.');
  process.exit(1);
}

const API = 'https://api.amodb.dev/3';
const IMG = 'https://img.amodb.dev/t/p/w500';
const headers = { Authorization: `Bearer ${AMODB_KEY}` };
const cache = new Map();

class AmodbError extends Error {
  constructor(status) {
    super(`amodb returned ${status}`);
    this.status = status;
  }
}

async function getJson(path, params, ttlMs) {
  const url = new URL(API + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
  const id = url.toString();

  const hit = cache.get(id);
  if (hit && hit.expires > Date.now()) return hit.data;

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(id, { headers, signal: AbortSignal.timeout(10_000) });
    if (res.status === 429 && attempt < 2) {
      const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 30);
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (!res.ok) throw new AmodbError(res.status);
    const data = await res.json();
    if (cache.size > 500) cache.delete(cache.keys().next().value);
    cache.set(id, { data, expires: Date.now() + ttlMs });
    return data;
  }
}

function buildCaption(movie) {
  const year = (movie.release_date || '').slice(0, 4) || 'year unknown';
  const imdb = movie.ratings?.imdb;
  const rating = imdb
    ? `IMDb ${imdb.value}`
    : movie.vote_average ? `TMDB ${movie.vote_average.toFixed(1)}` : 'not known';
  const lines = [`${movie.title} (${year})`, `Rating: ${rating}`];
  if (movie.runtime) lines.push(`Runtime: ${movie.runtime} min`);
  if (movie.overview) lines.push('', movie.overview.slice(0, 600));
  return lines.join('\n').slice(0, 1024);
}

const bot = new Bot(BOT_TOKEN);

bot.command('start', (ctx) =>
  ctx.reply('Send /movie and a title, for example /movie fight club.'));

bot.command('movie', async (ctx) => {
  const title = ctx.match.trim();
  if (!title) return ctx.reply('Usage: /movie fight club');
  await ctx.replyWithChatAction('typing');

  const found = await getJson('/search/movie', { query: title, language: 'en-US' }, 10 * 60_000);
  const hit = found.results[0];
  if (!hit) return ctx.reply(`No movie found for "${title}".`);

  const movie = await getJson(
    `/movie/${hit.id}`,
    { language: 'en-US', append_to_response: 'ratings' },
    6 * 60 * 60_000,
  );
  const caption = buildCaption(movie);
  if (!movie.poster_path) return ctx.reply(caption);
  try {
    await ctx.replyWithPhoto(IMG + movie.poster_path, { caption });
  } catch {
    await ctx.reply(caption);
  }
});

bot.catch(async ({ ctx, error }) => {
  console.error('Update failed:', error);
  if (error instanceof AmodbError && error.status === 401) {
    console.error('amodb rejected AMODB_KEY (401). Fix the key and restart.');
    await ctx.reply('The bot key was rejected. The owner has to fix it.').catch(() => {});
    process.exit(1);
  }
  const text = error instanceof AmodbError && error.status === 429
    ? 'Too many requests right now. Try again in a minute.'
    : 'The movie service did not answer. Try again later.';
  await ctx.reply(text).catch(() => {});
});

await bot.api.setMyCommands([{ command: 'movie', description: 'Find a movie by title' }]);
process.once('SIGINT', () => bot.stop());
process.once('SIGTERM', () => bot.stop());
bot.start({ onStart: (me) => console.log(`Running as @${me.username}`) });
```

Avvialo con il file `.env`, poi invia `/movie fight club` al tuo bot in Telegram.

```bash
node --env-file=.env bot.js
```

Per `/movie fight club` il primo risultato della ricerca è il film 550. Il bot invia la sua locandina, `https://img.amodb.dev/t/p/w500/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg`, con questa didascalia:

```text
Fight Club (1999)
Rating: IMDb 8.8
Runtime: 139 min

A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.
```

Il bot usa il long polling, quindi non ha bisogno di un indirizzo pubblico. Tieni il processo attivo con lo strumento che già usi per i servizi.

## Prossimi passi

- Aggiungi `/trending` con la chiamata `trending/movie/week` di [tendenze e liste](https://amodb.dev/it/trending-lists/).
- Lascia che gli utenti scelgano uno tra cinque risultati con una tastiera inline, usando più a fondo la risposta di [ricerca](https://amodb.dev/it/search/).
- Invia i trailer con l'endpoint [video e trailer](https://amodb.dev/it/videos-trailers/).
- Costruisci lo stesso comando per [Discord](https://amodb.dev/it/discord-movie-bot/).

## Domande e risposte

### Il bot ha bisogno di un server pubblico o di un webhook?

No. Il bot usa il long polling tramite bot.start(), quindi fa solo richieste in uscita. Eseguilo su qualsiasi macchina che resti online. Un webhook è facoltativo e serve solo se preferisci la consegna in push.

### Quante richieste amodb usa un comando /movie?

Due: una chiamata di ricerca e una chiamata di dettagli con le valutazioni. Un titolo ancora nella cache Map non ne usa nessuna. Con il piano da 20 richieste al minuto bastano per circa 10 nuove ricerche al minuto.

### Perché a volte la locandina non viene inviata?

Telegram scarica la foto dall'URL da solo, e rifiuta i file che non riesce a recuperare o che superano i suoi limiti. Il bot intercetta quell'errore e invia la didascalia come testo semplice invece di restare in silenzio.

### Il bot può rispondere in un'altra lingua?

Sì. Cambia il valore di language, per esempio ru-RU o es-ES, in entrambe le chiamate. Sono servite quattordici lingue di base, e qualsiasi altro tag ripiega sull'inglese. Titoli e trame arrivano tradotti quando esiste una traduzione.

### Dove devo tenere il token del bot e la chiave API?

In variabili d'ambiente, caricate con l'opzione env-file di Node.js o con il tuo gestore di processi, e mai in Git. Se il token del bot trapela, creane uno nuovo con BotFather. Per una chiave amodb, creane una nuova nella dashboard del tuo account.

## Pagine correlate

- [Tutorial bot Discord per film](https://amodb.dev/it/discord-movie-bot/): Crea un bot Discord con discord.js v14 e un comando slash /movie che risponde con un embed di locandina, valutazioni e trama da amodb, con retry su 429.
- [Tutorial API film JavaScript](https://amodb.dev/it/javascript-movie-api/): Uno script Node.js che carica dettagli, crediti e valutazioni di un elenco di ID film con una richiesta ciascuno, cache TTL, retry su 429 e zero dipendenze.
- [API di ricerca](https://amodb.dev/it/search/): API di ricerca con percorsi e JSON di TMDB v3. Trova film, serie, persone, collezioni, società e parole chiave per testo, con filtri anno e paginazione.
- [Valutazioni di film e serie TV](https://amodb.dev/it/ratings/): API valutazioni film: punteggi IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB di un titolo in una chiamata, con null per le fonti non note.
- [Limiti di richieste](https://amodb.dev/it/rate-limits/): amodb limita ogni chiave per richieste al minuto in una finestra scorrevole di 60 secondi: 20, 40 o 120 in base al piano. Leggi gli header, gestisci il 429.
- [Autenticazione](https://amodb.dev/it/authentication/): Come inviare la chiave amodb: api_key nella query, un header Authorization Bearer o apikey per le chiamate OMDb. Formato, conservazione, rotazione, errori 401.

Aggiornato: 2026-10-18. Source: https://amodb.dev/it/telegram-movie-bot/
