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. 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:
BOT_TOKEN=YOUR_BOT_TOKEN
AMODB_KEY=YOUR_API_KEYPasul 2: creează proiectul
Setarea type activează sintaxa import și await la nivel superior. Pagina autentificare arată formele cheii în antet și în api_key.
mkdir movie-bot && cd movie-bot
npm init -y
npm pkg set type=module
npm install grammyPasul 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ă.
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_ și construiește o legendă. URL-ul posterului este https:/ 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.
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 explică toate cele cinci surse.
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);
}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.
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 și erori pentru detalii.
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();
}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.
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.
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.
node --env-file=.env bot.jsPentru /movie fight club, prima potrivire din căutare este filmul 550. Botul trimite posterul lui, https:/, cu această legendă:
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.
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.Pașii următori
- Adaugă
/trendingcu apelultrending/movie/weekdin tendințe și liste. - Lasă utilizatorii să aleagă unul din cinci rezultate cu o tastatură inline, folosind mai mult din răspunsul de căutare.
- Trimite trailere cu endpoint-ul videoclipuri și trailere.
- Construiește aceeași comandă pentru Discord.