Tutoriel bot Telegram de films

Ce tutoriel construit un bot Telegram avec grammY sur Node.js. Envoyez /movie fight club et le bot répond avec l'affiche, l'année, la durée et la note d'amodb. Il lit les deux jetons dans des variables d'environnement, réessaie sur 429 avec Retry-After, met les réponses en cache en mémoire et s'arrête sur une clé rejetée. Il prend environ 20 minutes.

.js Telegram Mis à jour le Voir en Markdown

Ce que vous construisez

Vous construisez un bot Telegram qui répond à /movie fight club avec l'affiche, le titre, l'année, la durée et la note. Il vous faut Node.js 20.6 ou plus récent, un jeton de bot obtenu auprès de BotFather dans Telegram et une clé amodb. Le bot utilise grammY et le fetch intégré : un seul paquet npm suffit. Prévoyez environ 20 minutes.

Étape 1 : obtenir deux identifiants

Choisissez une offre sur les tarifs. L'offre fixe combien de requêtes par minute votre bot peut envoyer. Créez une clé dans le tableau de bord de votre compte, longue de 32 caractères hexadécimaux. Ouvrez ensuite une conversation avec BotFather dans Telegram, envoyez /newbot, répondez aux deux questions et copiez le jeton qu'il affiche.

Placez les deux valeurs dans un fichier .env et ajoutez ce fichier à .gitignore :

Shell
BOT_TOKEN=YOUR_BOT_TOKEN
AMODB_KEY=YOUR_API_KEY

Étape 2 : créer le projet

Le réglage type active la syntaxe import et l'await de niveau supérieur. La page authentification présente les formes en en-tête et en api_key de la clé.

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

Étape 3 : répondre à la première commande

grammY place le texte qui suit une commande dans ctx.match. Commencez par une fonction d'aide qui envoie l'en-tête Bearer et une commande qui cherche.

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

Étape 4 : construire la réponse avec une affiche

Prenez le premier résultat de recherche, chargez-le avec append_to_response=ratings et construisez une légende. L'URL de l'affiche est https://img.amodb.dev/t/p/w500 plus poster_path. Telegram télécharge lui-même la photo, et img.amodb.dev n'exige aucune clé. Une légende contient au plus 1024 caractères : le code coupe donc le résumé.

Chaque source de ratings est un objet avec value et votes, ou null. Quand imdb vaut null, la légende retombe sur le vote_average agrégé. La page notes explique les cinq sources.

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

Étape 5 : gérer les erreurs et les limites de débit

Laissez les gestionnaires lever des exceptions et interceptez tout à un seul endroit. Sans bot.catch, grammY arrête le bot quand un gestionnaire lève une exception.

CasCauseRéaction du bot
401Clé absente, fausse ou suspendueLa consigner, prévenir la conversation, quitter. Chaque commande échouerait.
429Au-dessus de vos requêtes par minuteAttendre Retry-After secondes, plafonnées à 30, réessayer deux fois, puis demander à l'utilisateur de réessayer plus tard.
results videAucun titre ne correspond à la requêteRépondre qu'aucun film n'a été trouvé.
Délai dépassé ou erreur réseaufetch lève une exceptionRépondre par de brèves excuses et consigner l'erreur.
Affiche rejetéeTelegram ne peut pas récupérer l'imageEnvoyer la légende en texte.

La boucle ci-dessous se trouve dans getJson. AmodbError est une petite sous-classe de Error avec un champ status, visible dans le listing final.

Chaque commande coûte deux requêtes : l'offre à 20 requêtes par minute sert donc environ 10 nouvelles recherches par minute. Voir limites de débit et erreurs pour les détails.

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

Étape 6 : mettre les réponses en cache

La vérification de la limite de débit passe avant le cache de périphérie : seul un cache dans votre propre bot économise des requêtes. Gardez une Map de l'URL vers les données et une heure d'expiration. Utilisez les durées de la périphérie : 10 minutes pour la recherche et 6 heures pour les détails. Un plafond de taille empêche un bot qui tourne longtemps de grossir sans limite.

Les lignes du cache se trouvent aussi dans getJson.

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

Étape 7 : le bot complet

Enregistrez ceci sous bot.js.

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

Démarrez-le avec le fichier .env, puis envoyez /movie fight club à votre bot dans Telegram.

Shell
node --env-file=.env bot.js

Pour /movie fight club, le premier résultat de recherche est le film 550. Le bot envoie son affiche, https://img.amodb.dev/t/p/w500/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg, avec cette légende :

Le bot utilise l'interrogation longue : il n'a donc besoin d'aucune adresse publique. Gardez le processus actif avec l'outil que vous utilisez déjà pour les services.

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.

Pour aller plus loin

  • Ajoutez /trending avec l'appel trending/movie/week de tendances et listes.
  • Laissez les utilisateurs choisir l'un de cinq résultats avec un clavier en ligne, en exploitant davantage la réponse de recherche.
  • Envoyez des bandes-annonces avec le point de terminaison vidéos et bandes-annonces.
  • Construisez la même commande pour Discord.