Un Bot Telegram... con Laravel!

Per uno sviluppatore, i Bot sono uno degli argomenti più ricorrenti in questo periodo. Come costruirne uno, usando Laravel? Scopriamolo insieme...
francesco
Francesco Malatesta
06/04/2017 in Package, Tutorial

Se non abbiamo passato gli ultimi due anni in una grotta, sicuramente ci siamo imbattuti in una parola: Bot.

Wikipedia dice: Il bot (abbreviazione di robot) in terminologia informatica in generale è un programma che accede alla rete attraverso lo stesso tipo di canali utilizzati dagli utenti umani (per esempio che accede alle pagine Web, invia messaggi in una chat, si muove nei videogiochi, e così via). Programmi di questo tipo sono diffusi in relazione a molti diversi servizi in rete, con scopi vari ma in genere legati all'automazione di compiti che sarebbero troppo gravosi o complessi per gli utenti umani.

C'è da dire che il bot non è un concetto nuovo. Nell'ultimo periodo però, con l'affermarsi del machine learning e grazie a realtà come Facebook e Telegram, una nuova generazione di bot sta prendendo piede. Si tratta fondamentalmente di bot con cui possiamo chattare, e sulla base di questo input ottenere un output specifico.

Gli usi sono molteplici: basti pensare a Bot che si possono integrare con servizi web specifici, o magari con altri servizi nel mondo IoT, e così via. Gli usi sono praticamente illimitati. Non a caso, è possibile trovare bot di ogni tipo.

Come è facile immaginare, di conseguenza, non mancano all'appello SDK e strumenti di ogni genere per svilupparne a volontà. Un ottimo punto di partenza, per chi volesse iniziare a farsi una cultura in materia, è Chat Bots Magazine.

Quando ho iniziato a muovere i primi passi in questo ambito, chiaramente, mi sono chiesto: quanto è difficile creare un bot?

La risposta è ovviamente "dipende", ma se si vuole iniziare ci sono strumenti per qualsiasi linguaggio, framework e tecnologia.

Laravel e PHP, ovviamente, non fanno eccezione!

Un paio di cose importanti prima di proseguire:

Quale Bot?

Prima di mettere le mani sul codice però, fermiamoci un secondo. Non ancora abbiamo scelto cosa vogliamo realizzare!

Nessuna idea? No problem, ne ho una io!

Da qualche tempo sto seguendo con interesse il mondo delle cryptocurrency. Soprattutto Bitcoin ed Ethereum, che allo stato attuale sono quelle più "quotate". Perchè non costruire un semplice bot al quale poter scrivere per chiedere il prezzo di un certo token (Bitcoin o Ethereum) in quel momento?

Niente di particolarmente complesso a livello di interazione.

Il nostro bot si comporterà così:

  • se il messaggio inviato al bot è /quote eth, l'utente riceverà un messaggio con il prezzo di Ethereum (in €) in quel momento;
  • se il messaggio inviato al bot è /quote btc, l'utente riceverà un messaggio con il prezzo di Bitcoin (in €) in quel momento;
  • qualsiasi altro messaggio inviato al bot che non sia uno dei due appena visti permetterà all'utente di ricevere un messaggio di "aiuto" per capire cosa fare;

Come caso base è più che sufficiente per iniziare a progettare qualcosa. Chiaramente servirà una fonte dalla quale attingere il prezzo di volta in volta: userò le ottime API di Coinbase, uno degli exchange più famosi.

Per i non addetti ai lavori, un exchange è una piattaforma sulla quale poter comprare e vendere delle cryptocurrency. Per ciò di cui abbiamo bisogno non avremo la necessità di implementare nessun tipo di autorizzazione per le API, il che significa che potremo concentrarci al meglio sul codice e sul bot.

Pronti?

Ah, prima che mi dimentichi: tutto il codice è disponibile in questo repository.

Registrare il Bot

Per poter creare un Bot Telegram c'è bisogno innanzitutto di registrarlo... usando un altro bot!

Basta, infatti, contattare su Telegram il buon BotFather. Non si deve fare altro che scrivergli qualcosa, anche solo "ciao" ed inviare il messaggio per ricevere un messaggio con tutti i comandi da usare per poterlo usare.

Quello che a noi interessa è /newbot, che ci consentirà di registrare un nuovo bot nel database di Telegram.

Bisognerà specificare un nome e scegliere un "nickname" che verrà poi usato per identificare univocamente il bot. Una volta terminata la procedura, BotFather ci manderà un token, molto importante perchè sarà la chiave per le API di cui avremo bisogno per leggere i messaggi in arrivo del bot, rispondere agli utenti e così via.

Una volta completata la registrazione, inoltre, potremo usare BotFather per personalizzare ulteriormente il nostro bot: dalla modifica della descrizione all'immagine del profilo, e così via.

Implementare il Bot

Arriviamo finalmente al punto. Come si fa a creare un bot Telegram con Laravel?

Facendo una semplice ricerca vengono fuori vari package e guide. Noi, oggi, ci concentreremo sul package più famoso, Telegram Bot SDK di Irazasyed.

In questo articolo scopriremo come usarlo per raggiungere il nostro obiettivo.

Ambiente di Lavoro

Da qualche parte bisogna iniziare: direi dall'ambiente di lavoro! Creiamo un nuovo progetto Laravel usando il sistema che preferiamo. Per questo tutorial ho usato uno script che mi sono scritto, compatibile sia con Linux che con MacOS: LaraPrep!

Una volta creato l'ambiente, possiamo passare a...

Installare il Package

Per installare il package basta un

$ composer require irazasyed/telegram-bot-sdk

ed un po' di configurazione. In primis l'aggiunta del service provider in config/app.php

    Telegram\Bot\Laravel\TelegramServiceProvider::class,

ed eventualmente la Facade. Non è obbligatoria comunque.

    'Telegram'  => Telegram\Bot\Laravel\Facades\Telegram::class,

Dopodichè c'è da pubblicare i vari file del package con un

$ php artisan vendor:publish --provider="Telegram\Bot\Laravel\TelegramServiceProvider"

Volendo basta anche un semplice

$ php artisan vendor:publish

visto che l'aggiunta del flag --provider serve a scegliere per quali vendor copiare i file.

Non rimane che l'ultimo step: aggiungere il token del nostro Bot al file di configurazione config/telegram.php. Lo abbiamo ottenuto prima, durante la registrazione.

    ...
    'bot_token' => '1234:ABCD',
		...

Codice #1 - Il Primo Comando Telegram

Abbiamo configurato tutto: siamo pronti ad iniziare. La prima cosa che faremo sarà creare il comando di default, quello che verrà mostrato quando l'utente scriverà al bot /help, o /start.

Uno degli aspetti più interessanti del package che abbiamo appena installato è il sistema di "comandi" Telegram. In poche parole, è possibile definire dei comandi Telegram per il nostro bot come delle classi simili ai comandi Artisan che noi conosciamo già.

Una volta creato questo comando tutto quello che bisogna fare è registrarlo nel file config/telegram.php, nell'array commands.

Eccolo qui, il nostro HelpCommand, nella cartella app/TelegramCommands.

<?php

namespace TelegramBot\TelegramCommands;

use Telegram\Bot\Commands\Command;

class HelpCommand extends Command
{
    /**
     * @var string Command Name
     */
    protected $name = "help";

    /**
     * @var string Command Description
     */
    protected $description = "Comando di benvenuto, eseguito di default";
		
    /**
     * @inheritdoc
     */
    public function handle($arguments)
    {
        $this->replyWithMessage(['text' => 'Ciao! Sono TokenBot! Scrivi "/quote btc" per conoscere il prezzo di un Bitcoin, oppure "/quote eth" per conoscere il prezzo di un Ethereum.']);
    }
}

Si, tutto qui. Cerchiamo però di capire cosa succede:

  • la classe estende Telegram\Bot\Commands\Command, base di partenza per creare altri command per Telegram. Ha a corredo una serie di metodi utilissimi per poter interagire con gli utenti;
  • l'attributo $name definisce il comando. Di conseguenza, help equivarrà al comando /help inviabile da un utente. Un po' come accade già per le route, insomma;
  • perchè proprio help? Perchè è quello "di default" per Telegram in caso di invio di un comando, da parte dell'utente, non riconosciuto;
  • il metodo replyWithMessage appartiene alla classe base e consente di inviare all'utente che ha inviato il comando un semplice messaggio testuale come risposta;

Codice #2 - Il Comando Artisan "process"

Il primo comando Telegram c'è: tocca provarlo! Per farlo, ci "aiuteremo" con un semplice comando Artisan. Creiamolo al volo eseguendo

$ php artisan make:command ProcessCommand

che si occuperà di creare, appunto, un nuovo comando Artisan chiamato "ProcessCommand". Quello che dovrà fare sarà "gestire" le richieste in arrivo dagli utenti su Telegram. Per fortuna, il nostro package ha un metodo della facade Telegramche fa tutto in modo praticamente automatico: commandsHandler.

Il nostro comando Artisan apparirà così:

<?php

namespace TelegramBot\Console\Commands;

use Illuminate\Console\Command;

class ProcessCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'bot:process';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Processa i messaggi in arrivo del bot';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
      try {
        $result = \Telegram::commandsHandler(false);
        $this->info('Processati ' . count($result) . ' messaggi.');
      } catch (\Exception $e) {
        $this->error('Errore: ' . $e->getMessage());
      }
    }
}

Vediamo cosa succede qui:

  • il comando Artisan bot:process si occupa di chiamare commandsHandler, che chiama le API di Telegram per vedere se sono arrivati nuovi messaggi. Se sono arrivati, controlla se questi corrispondono a comandi definiti nell'applicazione;
  • se ci sono dei comandi da eseguire, per il comando specifico viene effettuata una chiamata al metodo handle;

Come è possibile notare, in questo caso stiamo facendo del long polling: ogni volta che verrà eseguito questo comando tratteremo "in blocco" i nuovi messaggi degli utenti.

Esiste anche un'altra modalità di interazione che prevede un webhook da configurare, ma per questa guida la eviteremo. C'è da tenere a mente però che in caso di bot più "carichi" di messaggi da gestire il webhook è praticamente una scelta obbligata.

Comunque sia, manca solo il comando "/quote", che useremo per ottenere la quotazione, in quello specifico momento, di una currency. Prima però, dobbiamo creare al volo un piccolo servizio per poterci interfacciare con Coinbase.

Codice #2 - Il Servizio Coinbase

Per creare un servizio connesso a Coinbase le possibili scelte sono due:

  • trovare un package che mi permetta di interagire con Coinbase;
  • scrivere al volo ciò che mi serve implementando chiamate HTTP con un client;

La soluzione migliore? Nel nostro caso la seconda: non dobbiamo fare nulla di complesso, a pensarci bene. Scaricare e configurare un package solo per recuperare un numero è sicuramente un overhead.

Questo numero sarà lo "spot price", un prezzo indicativo che le API ci mettono a disposizione (qui la documentazione, sotto "get spot price").

Avremo comunque bisogno di un client HTTP per poter lavorare: ho scelto Guzzle, installandolo con un semplice

$ composer  require guzzlehttp/guzzle

Una volta installato il package, possiamo procedere. Creiamo una nuova cartella in app, chiamata Services, ed al suo interno una nuova classe, Coinbase, in un file Coinbase.php.

<?php

namespace TelegramBot\Services;

use GuzzleHttp\Client;

class Coinbase
{
    const COINBASE_API_URL = 'https://api.coinbase.com/v2/';

    /** @var Client **/
    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function getPriceFor($currencyCode)
    {
      if(!in_array($currencyCode, ['BTC', 'ETH'])) {
        throw new \Exception('Currency code must be BTC or ETH');
      }

      $response  = $this->client->get(self::COINBASE_API_URL . 'prices/' . $currencyCode . '-EUR/spot');
      $decodedResponse = json_decode($response->getBody()->getContents(), true);

      return $decodedResponse['data']['amount'];
    }
}

Capiamo, riga dopo riga, cosa succede qui:

  • la prima costante dichiarata nella classe serve a tenere "separato" dal resto del codice l'url base che useremo per le nostre chiamate alle API;
  • nel costruttore della classe iniettiamo il client HTTP che useremo, Guzzle. Ci permetterà di effettuare le chiamate alle API di Coinbase con una sintassi semplicissima e semplice da comprendere;
  • nel metodo getPriceFor controlliamo innanzitutto se il valore di $currencyCode è "ETH" o "BTC" (gli unici due consentiti). Altri valori non sono consentiti, e verrà lanciata un'eccezione in tal caso. Se il controllo invece non rileva problemi effettuiamo la chiamata a Coinbase, verso l'endpoint formato dalla stringa "/prices", una combinazione di currency (in questo caso, quella da noi scelta e l'euro, per ottenere il prezzo in euro), ed il segmento finale /spot;
  • il metodo getPriceFor prende l'amount di cui abbiamo bisogno nell'array risultante (dopo aver fatto passare il JSON arrivato dalle API attraverso un json_decode);

Il valore ritornato, a questo punto, è esattamente quello di cui abbiamo bisogno.

Nota: essendo un tutorial a scopi puramente illustrativi, non ho implementato dei test optando invece per una gestione molto "semplificata" della cosa. In un'applicazione da mondo reale, per carità, scrivete i maledettissimi test.

Detto questo, non rimane altro che finire l'opera! Costruiamo il secondo comando Telegram, "/quote"!

Codice #3 - Il Comando Telegram "/quote"

Prima di metterci a costruire l'ultimo comando, c'è una piccola accortezza da prendere. Supponiamo di voler usare il nostro servizio appena creato. La sua costruzione è piuttosto semplice:

// il client guzzle di cui avremo bisogno
$client = new Client();

// il nostro servizio
$coinbase = new Coinbase($client);

Dopo questa costruzione possiamo usare il servizio come meglio crediamo. Tuttavia, possiamo fare di meglio!

Aggiungiamo, nel metodo boot del nostro TelegramBot\Providers\AppServiceProvider:

// dopo il namespace, all'inizio del file...
use GuzzleHttp\Client;
use TelegramBot\Services\Coinbase;

// nel metodo boot()
$this->app->bind(Coinbase::class, function(){
    return new Coinbase(new Client());
});

Cosa è successo? Semplice: stiamo istruendo il nostro service container riguardo come va creata un'istanza del nostro servizio Coinbase.

Ecco quindi che arriviamo al nostro comando Telegram: creiamolo, in app/TelegramCommands/QuoteCommand.php.

<?php

namespace TelegramBot\TelegramCommands;

use Telegram\Bot\Commands\Command;
use TelegramBot\Services\Coinbase;

class QuoteCommand extends Command
{
    /**
     * @var string Command Name
     */
    protected $name = "quote";

    /**
     * @var string Command Description
     */
    protected $description = "Restituisce la quotazione attuale di una currency";

    /**
     * @inheritdoc
     */
    public function handle($arguments)
    {
      /** @var Coinbase **/
      $coinbase = app(Coinbase::class);
      $currencyCode = trim(strtoupper($arguments));

      switch ($currencyCode) {
        case 'ETH':
        case 'BTC':
          $quote = $coinbase->getPriceFor($currencyCode);
          $this->replyWithMessage(['text' => 'Quotazione ' . $currencyCode . ': €' . $quote]);
          break;

        default:
          $this->replyWithMessage(['text' => 'Mmmh... non conosco questa currency! Specifica "eth" o "btc" dopo il /quote.']);
          break;
      }
    }
}

La logica è piuttosto semplice:

  • creiamo l'istanza del servizio Coinbase, usando il service container. Il service container di Laravel me lo restituirà esattamente come ci serve, perchè abbiamo definito un binding nel provider AppServiceProvider;
  • facciamo un po' di pulizia del dato in input, e se il valore è ETH o BTC viene effettuata una chiamata al servizio di Coinbase. In caso contrario, invece, viene inviato all'utente un messaggio di fallback, in modo tale da guidarlo verso l'uso migliore;

Non scordiamoci di registrarlo nel file config/telegram.php, nell'array commands.

Direi che ci siamo, ed il nostro bot è pronto!

... e ora?

Il bot è pronto a essere usato... e ora che succede?

Beh, volendo ci sono un sacco di idee interessanti da implementare: le lascio qui come "ispirazione".

Esercizio 1 - Questione di Risparmio

Quando si usa un'API è bene ottimizzare il numero di chiamate da fare. Sia per questioni di performance della propria applicazione, ma anche perchè alcuni servizi (giustamente) pongono un limite al numero di chiamate che si possono fare in un certo lasso di tempo.

Ecco un'ottima idea per un primo esercizio: un sistema che mette in cache il valore ottenuto dal servizio Coinbase per un certo tempo.

Suggerimenti:

  • usa il sistema di Cache di Laravel!

Esercizio 2 - Avvisami!

Un bot che controlla l'andamento di una cryptocurrency è comodo. Dopo un po' però è noioso scrivergli, magari più volte! Perchè non implementare un bel sistema di avvisi? Qualcosa come

  • se il valore della cryptocurrency X scende oltre Y, avvisami;
  • se il valore della cryptocurrency X sale oltre Y, avvisami;

Suggerimenti:

  • sarà necessario "registrare" gli utenti, in modo tale da avere a disposizione il loro ID univoco Telegram in futuro;
  • sarà necessario anche gestire gli avvisi (dove memorizzarli? quando eliminarli? ogni quanto fare i controlli? e così via...);

Buon lavoro ;)