ottobre 21, 2021 · NodeJS Telegram

Telegram bot in NodeJS: pulsantiera personalizzata e menu a più livelli

Una delle cose più interessanti che ho incontrato durante i miei esperimenti scrivendo un Telegram bot in NodeJS è la possibilità di mostrare all'interlocutore una pulsantiera personalizzata, e guidarlo nella scelta di una opzione all'interno di un menu a più livelli. Questo approccio si sposa molto bene con il tipico utilizzo di Telegram, che avviene attraverso un dispositivo mobile con uno schermo touch e di dimensioni limitate.

Gli aspetti di questo paradigma di utilizzo sono sostanzialmente due:

Nell'esempio che mostrerò qui sotto, tratto da mio CovidBot, voglio offrire all'utente la possibilità di richiedere i dati di contagio in una regione, attraverso il comando /regione nome, oppure di usare il comando /regione senza parametri e scegliere la regione attraverso un menu di navigazione a due livelli: area (nordest, nordovest ecc..) e poi la regione vera e propria.

Tutto inizia con la predisposizione del callback onText con una opportuna regexp.

bot.onText(/\/region[ie]?([ ]+([a-zA-Z]+))?/, async (msg, match) => {....

Il comando accetta in input:

Per spiegazioni più esaustive sul meraviglioso mondo delle regexp suggerisco questo link.

Il passo successivo è verificare se il secondo gruppo (quello interno, con il nome regione) è stato passato, e in tal caso gestirlo a dovere e concludere qui la procedura.

if (match[2]){
   ....
}

In caso negativo, l'utente non ha passato il nome della regione e vogliamo mostrare la pulsantiera con l'elenco delle aree.

// Lista aree (id univoco e testo descrittivo)
const AREAS = [
  {id: "nordovest", descr: "Nord-Ovest"},
  {id: "nordest", descr: "Nord-Est"},
  {id: "centro", descr: "Centro"},
  ...
]

const keyboard = AREAS.map(area => ({
  text: area.descr,
  callback_data: JSON.stringify({
    type: 'area',
    id_area: area.id
  })
}))

const opts = {
  reply_markup: {
    inline_keyboard: splitArray(keyboard, 3)
  }		
}

bot.sendMessage(msg.chat.id, 'Dati regionali. Seleziona area:', opts);

Nel codice qui sopra:

Adesso l'utente ha ricevuto il nostro messaggio con la tastiera custom.

Cosa succede quando viene premuto un pulsante? Viene chiamato nel bot un evento speciale, chiamato callback_query. Possiamo catturarlo con questa dicitura:

bot.on('callback_query', (callbackQuery) => {

  const msg = callbackQuery.message;
  const chat_id = msg.chat.id;
  const message_id = msg.message_id;

  const data = JSON.parse(callbackQuery.data);
  const {type} = data;

  switch(type){
    case 'area':
      manageAreaCallback(bot, chat_id, message_id, data);
      break;
      
......

  bot.answerCallbackQuery(callbackQuery.id);
}

Nella routine di gestione qui sopra, una volta catturato il callback andiamo a estrarne le informazioni principali:

Ora dobbiamo gestire il messaggio. Il meccanismo è analogo a prima: recuperiamo la lista delle regioni appartenenti a quell'area e inviamo un altro messaggio con una pulsantiera custom.

export function manageAreaCallback(bot, chat_id, message_id, data){

  const {id_area} = data;
  const area = REGIONS.find(area => area.id === id_area);
 
  const keyboard = area.regions.map(reg => ({
    text: reg.descr,
    callback_data: JSON.stringify({
      type: 'region',
      id_reg: reg.id,
      id_area: id_area
    })              
  }))
  
  keyboard.push({
    text: '<-',
    callback_data: JSON.stringify({
      type: 'areas_list'
    })
  })
  
  bot.editMessageText(
    `Hai chiesto regioni del ${area.descr}, seleziona regione:`,
    {
      chat_id: chat_id,
      message_id: message_id,
      reply_markup: {
        inline_keyboard: splitArray(keyboard, 3)
      }                   
    }
  )   

}

Le uniche particolarità di questo metodo sono:

Il risultato è come nell'immagine.

A questo punto il gioco è fatto. Andiamo ad arricchire il "callback_query" con le nuove casistiche e il gioco è fatto. Naturalmente in caso di callback con type "region" (pressione del tasto con il nome di una regione) dovremo inviare non più una tastiera ma un semplice messaggio con il contenuto informativo.

...
switch(type){
  case 'area':
    manageAreaCallback(bot, chat_id, message_id, data);
    break;
  case 'region':
    manageRegionCallback(bot, chat_id, message_id, data, regionalDataFull);
    break;
  case 'areas_list':
    manageAreasListCallback(bot, chat_id, message_id);
    break;      
}
...

Le modalità di invio del messaggio esulano dall'argomento di questo post. Per ulteriori dettagli tutto il codice del bot è pubblicato su github.

Spero che questa spiegazione sempliciotta sia servita al suo scopo, ovvero descrivere una possibilità e dare una indicazione di massima sul suo utilizzo in un caso concreto. Per ulteriori dettagli consiglio di rivolgersi alla documentazione ufficiale di node-telegram-bot-api, ricca di esempi e tutorial.

Alla prossima!