# Tutorial bot Telegram de filme

> Acest tutorial construiește un bot Telegram cu grammY pe Node.js. Trimite /movie fight club, iar botul răspunde cu posterul, anul, durata și evaluarea din amodb. Citește ambele token-uri din variabile de mediu, reîncearcă la 429 folosind Retry-After, ține răspunsurile în memorie și se oprește la o cheie respinsă. Durează cam 20 de minute.

## Ce construiești

Construiești un bot Telegram care răspunde la `/movie fight club` cu posterul, titlul, anul, durata și evaluarea. Ai nevoie de Node.js 20.6 sau mai nou, un token de bot de la BotFather din Telegram și o cheie amodb. Botul folosește grammY și `fetch` încorporat, deci un singur pachet npm ajunge. Socotește cam 20 de minute.

## Pasul 1: obține două credențiale

Alege un plan la [prețuri](https://amodb.dev/ro/pricing/). Planul stabilește câte cereri pe minut poate trimite botul tău. Creează o cheie în panoul contului tău, lungă de 32 de caractere hexazecimale. Apoi deschide o conversație cu BotFather în Telegram, trimite `/newbot`, răspunde la cele două întrebări și copiază token-ul afișat.

Pune ambele valori într-un fișier `.env` și adaugă acel fișier în `.gitignore`:

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

## Pasul 2: creează proiectul

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

Setarea `type` activează sintaxa `import` și `await` la nivel superior. Pagina [autentificare](https://amodb.dev/ro/authentication/) arată formele cheii în antet și în `api_key`.

## Pasul 3: răspunde la prima comandă

grammY pune textul de după o comandă în `ctx.match`. Începe cu un helper care trimite antetul Bearer și cu o comandă care caută.

```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();
```

## Pasul 4: construiește răspunsul cu un poster

Ia prima potrivire din căutare, încarcă-o cu `append_to_response=ratings` și construiește o legendă. URL-ul posterului este `https://img.amodb.dev/t/p/w500` plus `poster_path`. Telegram descarcă singur fotografia, iar `img.amodb.dev` nu cere cheie. O legendă poate avea cel mult 1024 de caractere, deci codul taie descrierea.

```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);
}
```

Fiecare sursă din `ratings` este un obiect cu `value` și `votes`, sau `null`. Când `imdb` este `null`, legenda revine la `vote_average` agregat. Pagina [evaluări](https://amodb.dev/ro/ratings/) explică toate cele cinci surse.

## Pasul 5: tratează erorile și limitele de cereri

Lasă handlerele să arunce și prinde totul într-un singur loc. Fără `bot.catch`, grammY oprește botul când un handler aruncă.

| Caz                         | Cauză                                     | Reacția botului                                                                                                          |
| --------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 401                         | Cheie lipsă, greșită sau suspendată       | O înregistrează în log, anunță conversația, iese. Orice comandă ar eșua.                                                 |
| 429                         | Ai depășit cererile pe minut              | Așteaptă `Retry-After` secunde, cu plafon de 30, reîncearcă de două ori, apoi cere utilizatorului să încerce mai târziu. |
| `results` gol               | Niciun titlu nu se potrivește interogării | Răspunde că nu s-a găsit niciun film.                                                                                    |
| Timeout sau eroare de rețea | `fetch` aruncă                            | Răspunde cu o scurtă scuză și înregistrează eroarea în log.                                                              |
| Poză respinsă               | Telegram nu poate prelua imaginea         | Trimite legenda ca text.                                                                                                 |

Bucla de mai jos se află în `getJson`. `AmodbError` este o mică subclasă `Error` cu un câmp `status`, arătată în listingul final.

```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();
}
```

Fiecare comandă costă două cereri, deci planul de 20 de cereri pe minut servește cam 10 căutări noi pe minut. Vezi [limite de cereri](https://amodb.dev/ro/rate-limits/) și [erori](https://amodb.dev/ro/errors/) pentru detalii.

## Pasul 6: păstrează răspunsurile în cache

Verificarea limitei de cereri rulează înaintea cache-ului la margine, deci doar un cache în propriul tău bot economisește cereri. Ține un `Map` de la URL la date și un moment de expirare. Folosește duratele de la margine: 10 minute pentru căutare și 6 ore pentru detalii. O limită de dimensiune împiedică un bot care rulează mult timp să crească fără limită.

Liniile de cache se află tot în `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 });
```

## Pasul 7: botul complet

Salvează asta ca `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}`) });
```

Pornește-l cu fișierul `.env`, apoi trimite `/movie fight club` botului tău în Telegram.

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

Pentru `/movie fight club`, prima potrivire din căutare este filmul 550. Botul trimite posterul lui, `https://img.amodb.dev/t/p/w500/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg`, cu această legendă:

```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.
```

Botul folosește long polling, deci nu are nevoie de o adresă publică. Ține procesul pornit cu unealta pe care o folosești deja pentru servicii.

## Pașii următori

- Adaugă `/trending` cu apelul `trending/movie/week` din [tendințe și liste](https://amodb.dev/ro/trending-lists/).
- Lasă utilizatorii să aleagă unul din cinci rezultate cu o tastatură inline, folosind mai mult din răspunsul de [căutare](https://amodb.dev/ro/search/).
- Trimite trailere cu endpoint-ul [videoclipuri și trailere](https://amodb.dev/ro/videos-trailers/).
- Construiește aceeași comandă pentru [Discord](https://amodb.dev/ro/discord-movie-bot/).

## Întrebări și răspunsuri

### Are botul nevoie de un server public sau de un webhook?

Nu. Botul folosește long polling prin bot.start(), deci face doar cereri de ieșire. Rulează-l pe orice mașină care rămâne online. Un webhook este opțional și util doar dacă preferi livrarea push.

### Câte cereri amodb folosește o comandă /movie?

Două: un apel de căutare și un apel de detalii cu evaluări. Un titlu care este încă în cache-ul Map nu folosește niciuna. Pe planul de 20 de cereri pe minut asta permite cam 10 căutări noi pe minut.

### De ce nu se trimite uneori posterul?

Telegram descarcă singur fotografia de la URL și respinge fișierele pe care nu le poate prelua sau care depășesc limitele lui. Botul prinde acea eroare și trimite în schimb legenda ca text simplu, în loc să tacă.

### Poate botul să răspundă în altă limbă?

Da. Schimbă valoarea language, de exemplu ru-RU sau es-ES, în ambele apeluri. Sunt servite paisprezece limbi de bază, iar orice altă etichetă revine la engleză. Titlurile și descrierile sosesc traduse când există o traducere.

### Unde ar trebui să țin token-ul botului și cheia API?

În variabile de mediu, încărcate cu opțiunea env-file din Node.js sau cu managerul tău de procese, și niciodată în Git. Dacă token-ul botului se scurge, creează unul nou cu BotFather. Pentru o cheie amodb, creează una nouă în panoul contului tău.

## Pagini conexe

- [Tutorial bot Discord de filme](https://amodb.dev/ro/discord-movie-bot/): Bot Discord cu discord.js v14 și comanda slash /movie, care răspunde cu un embed cu poster, evaluări și descriere din amodb. Include reîncercare la 429.
- [Tutorial API filme JavaScript](https://amodb.dev/ro/javascript-movie-api/): Scrie un script Node.js care încarcă detalii, credite și evaluări pentru o listă de filme, o cerere pe film, cu cache TTL, reîncercare la 429, fără dependențe.
- [API căutare](https://amodb.dev/ro/search/): API căutare cu căile și JSON-ul TMDB v3. Găsește filme, seriale, persoane, colecții, companii și cuvinte-cheie după text, cu filtre de an și paginare.
- [Evaluări pentru filme și TV](https://amodb.dev/ro/ratings/): API evaluări filme ce întoarce scorurile IMDb, Kinopoisk, Metacritic, Rotten Tomatoes și TMDB pentru un titlu într-un apel, cu null pentru sursele necunoscute.
- [Limite de cereri](https://amodb.dev/ro/rate-limits/): amodb limitează fiecare cheie în cereri pe minut, în fereastră glisantă de 60 de secunde: 20, 40 sau 120. Citește antetele și tratează 429 cu Retry-After.
- [Autentificare](https://amodb.dev/ro/authentication/): Cum trimiți cheia amodb: api_key în interogare, antetul Authorization Bearer sau apikey pentru apelurile OMDb. Format, stocare, rotire și erorile 401.

Actualizat: 2026-10-18. Source: https://amodb.dev/ro/telegram-movie-bot/
