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.
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 :
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é.
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.
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_ et construisez une légende. L'URL de l'affiche est https:/ 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.
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.
| Cas | Cause | Réaction du bot |
|---|---|---|
| 401 | Clé absente, fausse ou suspendue | La consigner, prévenir la conversation, quitter. Chaque commande échouerait. |
| 429 | Au-dessus de vos requêtes par minute | Attendre Retry-After secondes, plafonnées à 30, réessayer deux fois, puis demander à l'utilisateur de réessayer plus tard. |
results vide | Aucun titre ne correspond à la requête | Répondre qu'aucun film n'a été trouvé. |
| Délai dépassé ou erreur réseau | fetch lève une exception | Répondre par de brèves excuses et consigner l'erreur. |
| Affiche rejetée | Telegram ne peut pas récupérer l'image | Envoyer 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.
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.
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.
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.
node --env-file=.env bot.jsPour /movie fight club, le premier résultat de recherche est le film 550. Le bot envoie son affiche, https:/, 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.
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
/trendingavec l'appeltrending/movie/weekde 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.