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. 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:
BOT_TOKEN=YOUR_BOT_TOKEN
AMODB_KEY=YOUR_API_KEYPassaggio 2: crea il progetto
L'impostazione type attiva la sintassi import e l'await di primo livello. La pagina autenticazione mostra le forme della chiave come header e come api_key.
mkdir movie-bot && cd movie-bot
npm init -y
npm pkg set type=module
npm install grammyPassaggio 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.
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_ e costruisci una didascalia. L'URL della locandina è https:/ 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.
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 spiega tutte e cinque le fonti.
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);
}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.
Ogni comando costa due richieste, quindi il piano da 20 richieste al minuto serve circa 10 nuove ricerche al minuto. Vedi limiti di richieste ed errori per i dettagli.
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();
}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.
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.
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.
node --env-file=.env bot.jsPer /movie fight club il primo risultato della ricerca è il film 550. Il bot invia la sua locandina, https:/, con questa didascalia:
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.
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.Prossimi passi
- Aggiungi
/trendingcon la chiamatatrending/movie/weekdi tendenze e liste. - Lascia che gli utenti scelgano uno tra cinque risultati con una tastiera inline, usando più a fondo la risposta di ricerca.
- Invia i trailer con l'endpoint video e trailer.
- Costruisci lo stesso comando per Discord.