11 Marzo, 2019 | Di

Come integrare le Telegram Bot API in Drupal 8 (Parte 3)

Drupalgram

Ben ritrovati all'ultima parte della serie di articoli sull'integrazione delle Bot API di Telegram in Drupal 8 mediante la realizzazione di un modulo personalizzato!

Nei capitoli precedenti...

Nelle precedenti due puntate abbiamo creato un nuovo servizio per Drupal (rendendolo noto al Service Container tramite il file drupalgram.services.yml) che svolge il ruolo di fornire un'interfaccia con le API dell'SDK Telegram che si è deciso di usare.

Abbiamo poi realizzato una form di configurazione, associata ad una rotta (/admin/drupalgram/config), la quale ci permette di impostare il token del bot (generato durante il primo articolo) e la chat ID (il nostro contatto telegram) al quale il bot recapiterà i suoi messaggi.

Cosa manca?

Nella parte 2 ci eravamo lasciati con queste parole: << implementeremo un blocco che espone una form di contatto al submit della quale il nostro bot ci notificherà tramite messaggio privato su telegram dell'azione svolta tramite il nostro sito >>.

Molto semplice, abbiamo bisogno di altri due elementi: una Form (la loro implementazione l'abbiamo già affrontata nell'articolo precedente) ed un Blocco! Sarà necessario, inoltre, modificare il codice servizio drupalgram.telegram_api per aggiungere il metodo sendDocument del quale ne vedremo a breve l'utilità.

Form

Creiamo quindi la nostra nuova form BotContactForm la quale verrà poi richiamata e renderizzata nel blocco.

Per farlo, come già detto in precedenza, bisogna rispettare il path drupalgram/src/Form/BotContactForm.php sul quale mapperà il namespace Drupal\drupalgram\Form.

<?php
 
namespace Drupal\drupalgram\Form;
 
use Drupal\block\Entity\Block;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\FormBase;
 
class BotContactForm extends FormBase {
  // ID della form.
  public function getFormId() {
    return 'bot_contact_form';
  }
 
  // Metodo per la costruzione dei campi componenti la form.
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Definisco campo di tipo "textfield" per l'oggetto del messaggio. (vedi Form API di Drupal 8)
    $form['subject'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Subject'),
      '#required' => TRUE,
      '#attributes' => [
        '#placeholder' => $this->t('Subject')
      ],
    ];
 
    // Definisco campo di tipo "textfield" per il corpo del messaggio. (vedi Form API di Drupal 8)
    $form['body'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Message'),
      '#required' => TRUE,
      '#attributes' => [
        'placeholder' => $this->t('Write here your message...')
      ]
    ];
 
    // Definisco campo di tipo "managed_field" (vedi Form API di Drupal 8) per delegare la gestione dell'entità File a Drupal
    $form['attachment'] = [
      '#type' => 'managed_file',
      '#title' => $this->t('Attachment'),
      '#upload_location' => 'public://drupalgram-attachments',
      '#multiple' => FALSE,
      '#description' => $this->t('Attach here a file (.pdf .doc .docx .png).'),
      '#upload_validators' => [
        'file_validate_size' => [ 5242880 ],
        'file_validate_extensions' => array('pdf doc docx png'),
      ],
    ];
 
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit')
    ];
 
    return $form;
  }
 
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // todo: implementare il corpo del metodo submitForm.
  }
}

Nel codice di sopra abbiamo definito all'interno del metodo buildForm (come già fatto durante la parte 2 di questo tutorial) i campi componenti la nostra form di contatto: subjectbodyattachmentsubmit. I primi due, a differenza di "attachment", sono resi obbligatori tramite la chiave #required valorizzata con TRUE perchè si ipotizza che l'utente interessato a contattarci non debba essere obbligato a spedirci necessariamente un allegato. 

Riguardo al campo attachment vediamo che la tipologia specificata à managed_field questo indica a Drupal che tale campo dovrà essere gestito tramite backend. Ovverò verrà tracciato nel database di Drupal il quale vedrà tale file come una File Entity. L'upload_location specifica in qule cartella verrà salvato il file caricato. Vi sono infine i validatori (#upload_validators) che si occupano, nel nostro caso, di verificare che:

  • la dimenzione del file - file_validate_size - non sia maggiore di 5MB (espressi un byte)
  • le estensione del file - file_validate_extensions - sia tra quelle elencate nell'array (pdf doc docx png).

Passiamo ora a scrivere il comportamento da eseguire al submit della form. Ovvero diamo un corpo al metodo submitForm. Abbiamo detto che al submit della form dovrà essere recapitato, all'utente corrispondente al chat ID configurato in precedenza, il contenuto della submission. Per farlo, dobbiamo sfruttare il metodo sendMessage implementato nel servizio drupalgram.telegram_api. Ci limiteremo, ora, ad inviare i soli campi required, subject e body, per poi concentrarci sull'invio dell'allegato.

Procediamo, dunque, con i seguenti passi:

  • istanziare il servizio drupalgram.telegram_api in una variabile $telegramService
  • prelevare dalla $form_state i valori subject e body
  • valorizzare una variabile $message nella quale comporremo il messaggio comprensivo di subject e body
  • inviare, tramite $telegramService il messaggio composto
  • mostrare un messaggio di stato per indicare il corretto invio del contatto.

Modifichiamo il metodo submitForm inserendovi il codice seguente:

    $telegramService = \Drupal::service('drupalgram.telegram_api');
    $subject = $form_state->getValue('subject');
    $body = $form_state->getValue('body');
 
    $message = $this->t("<b>Subject</b>: %subject", ['%subject' => $subject]) . PHP_EOL . PHP_EOL;
    $message .= $this->t("<b>Message</b>: %body\n", ['%body' => $body]);
 
    // Invia il messaggio con il parametro "parsemode" impostato ad HTML.
    $telegramService->sendMessage($message, 'HTML');
 
    // Mostra il messaggio di stato
    drupal_set_message($this->t('Thanks! Your message was been sent successfully.'));

Inviamo anche l'allegato!

Fatto questo non ci resta che implementare e gestire l'invio dell'allegato. Sfrutteremo a tal proposito il metodo, messo a disposizione dall'SDK Telegram PHP che stiamo utilizzando, sendDocument, il quale verrà eseguito, nel metodo submitForm, subito dopo aver spedito il messaggio.  

Prima però...

Ricordate lo scopo per il quale avevamo implementato il servizio drupalgram.telegram_api? Rileggendo la parte 2 di questo tutorial ci imbattiamo nella seguente frase: <<svolge il ruolo di fornire un'interfaccia con le API dell'SDK Telegram>>. Ebbene dal momento in cui abbiamo designato il nostro servizio quale interfaccia con le API dell'SDK Telegram è opportuno estendere quest'ultimo al fine di gestire anche la possibilità di richiamare il metodo sendDocument (come fatto in precedenza per sendMessage). 

Aggiungiamo quindi al nostro servizio (drupalgram/src/Services/TelegramAPI/TelegramAPI.php) il seguente metodo:

  /**
   * Il metodo seguente verrà usato per inviare
   * un file. Esso sfrutta a sua volta
   * sendDocument dell'SDK passandogli i parametri
   * chat_id e document - ovvero l'url del file da allegare.
   */
  public function sendDocument($file_url) {
    try {
      $file = InputFile::create($file_url, 'attachment');
      $this->telegram->sendDocument([
        'chat_id' => $this->TELEGRAM_CHAT_ID,
        'document' => $file
      ]);
    } catch (TelegramSDKException $exception) {
      drupal_set_message($exception->getMessage());
    }
  }

Come possiamo vedere dal codice del metodo di sopra è stato necessario creare, tramite il metodo statico create della classe (da importare) InputFile, una file resource (quella che solitamente otteniamo tramite un fopen, per es.) a partire dall'url in $file_url. L'altro parametro passato a create è il nome da dare al file creato (questo risulterà come nome del file allegato su telegram), nel nostro caso sarà "attachment".

 

Rimeno ora un'ultima modifica da eseguire al metodo submitForm per poi passare, in via conclusiva, alla realizzazione del blocco per esporre, in qualsiasi regione si voglia, la nostra form di contatto.

Modifichiamo, intanto, il metodo submitForm come segue (vi ricordo che tutti le classi utilizzate nel metodo devono essere prima incluse mediante la parola chiave use):

    $telegramService = \Drupal::service('drupalgram.telegram_api');
    $subject = $form_state->getValue('subject');
    $body = $form_state->getValue('body');
    $file = $form_state->getValue('attachment');
    $file_url = NULL;
 
    if (isset($file)) {
      // Carico il file tramite il suo fid.
      $file = File::load($file[0]);
      // Calcolo il suo url.
      $file_url = $file->url();
    }
 
    $message = $this->t("<b>Subject</b>: %subject", ['%subject' => $subject]) . PHP_EOL . PHP_EOL;
    $message .= $this->t("<b>Message</b>: %body\n", ['%body' => $body]);
 
    // Invia il messaggio con il parametro "parsemode" impostato ad HTML.
    $telegramService->sendMessage($message, 'HTML');
 
    // Invia il file.
    if ($file_url !== NULL) {
      $telegramService->sendDocument($file_url);
    }
 
    // Mostra il messaggio di stato
    drupal_set_message($this->t('Thanks! Your message was been sent successfully.'));

Piazziamo la form in un blocco

Per poter visualizzare la nostra form di contatto, abbiamo deciso di sfruttare, data la sua potenzialità, un blocco custom. Ciò permette di piazzare la nostra form di contatto in una o più regioni del nostro sito e gestirne le regole di visibilità comuni a tutti i Plugin Blocco. Si, avete sentito bene: plugin!

Drupal 8 infatti implementa nel core alcune parti principali del framework PHP Symfony 2, tra queste vi è anche il plugin system. Riporto brevemente quanto si dice nella documentazione di symfony 2 riguardo i plugin: <<Un plugin symfony offre un modo per pacchettizzare e distribuire un sottoinsieme di file che compongono il progetto. Un plugin può essere composto da classi, helper, configurazioni, web assets, ecc...>>. 

Non mi soffermo ulteriormente sui plugin, non essendo questo l'argomento del tutorial, ciò che ci serve sapere, però, è come Drupal riconosce i plugin.

Intanto tutti i plugin, in Drupal 8, dovranno obbligatoriamente risiedere nella cartella src/Plugin/<tipo-di-plugin> del modulo che vuole implementarlo. Nel nostro caso sarà: drupalgram/src/Plugin; il tipo di plugin che vogliamo implementare è un Blocco e lo faremo all'interno del file BotContactFormBlock.php. Il path completo è: drupalgram/src/Plugin/Block/BotContactFormBlock.php raggiungibile al namespace Drupal\drupalgram\Plugin\Block.

L'altra cosa che ci serve sapere sono le annotazioni, nel caso del blocco, l'annotazione @Block: queste sono particolari doc-block, blocchi di documentazione, implementati da doctrine (per avere più informazioni vedi la documentazione di doctrine), i quali, in Drupal 8, vengono usati per specificare alcuni meta-dati inerenti al blocco che si sta implementando, senza i quali Drupal non sarebbe in grado di "riconoscerlo" e renderlo disponibile tramite l'interfaccia web offerta dalla sezione Layout dei blocchi. Volendo fare un'analogia con Drupal 7, l'annotazione, nel caso del plugin blocco, corrisponde al hook_block_info.

La classe che andremo a creare, BotContactFormBlock, estenderà la classe BlockBase (questa ci obbligherà ad implementare il metodo build). Definiremo quindi il metodo build delegato a restituire un render array da mostrare, nel nostro caso restituiremo la form BotContactForm.

Quanto detto si sintetizza nel seguente codice:

<?php
 
namespace Drupal\drupalgram\Plugin\Block;
 
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Block\BlockBase;
use Drupal\drupalgram\Form\BotContactForm;
 
/**
 * Class BotContactFormBlock
 *
 * @package Drupal\drupalgram\Plugin\Block
 *
 * @Block(
 *   id = "bot_contact_form_block",
 *   admin_label = @Translation("Telegram Bot Contact Form"),
 *   category = @Translation("Drupalgram"),
 * )
 */
class BotContactFormBlock extends BlockBase {
  public function getFormBotContactForm() {
    return \Drupal::formBuilder()->getForm(BotContactForm::class);
  }
 
  public function build() {
    return $this->getFormBotContactForm();
  }
}

L'annotazione @Block definisce  l'id del blocco (il nome macchina), la sua label d'amministrazione (il nome che ci mostrerà nel layout dei blocchi) e la categoria. Il risultato è visibile nello screenshot seguente

Blocco custom

Posizioniamo il blocco e effettuiamo un test!

Procediamo dunque nel posizionare il nostro blocco nella regione che preferiamo. Io l'ho posizionato nella regione content.

Cosa succede se compiliamo la form e la inviamo? Quello che dovrebbe succedere è di ricevere un messaggio privato, su telegram, dal nostro bot nel quale è riportato (come visibile nello screenshot subito sotto) il contenuto della form.

Telegram

Come potrete notare il nostro modulo ha fatto quanto previsto. Il bot ci ha riportato il contenuto dei campi subject, body e ci ha allegato infine il documento rinominato come attchment, inseriti tramite la form di contatto esposta tramite il blocco custom BotContactFormBlock.

Concludendo...

Ed eccoci giunti alla conclusione di questa serie di articoli. Prima di salutarci sento la necessità di allertare i lettori circa la poca eleganze ed efficienza del codice scritto. Tanto per intenderci il codice scritto durante il corso di questa serie di articoli non passerebbe nessun quality test e certamente non vedrebbe (per fortuna!) l'approvazione, da parte della community Drupal, affinchè possa contribuire come modulo contrib. Questo perchè lo scopo di questa serie non era quello di scrivere un modulo da pubblicare su drupal.org bensì quello di far emergere le potenzialità offerte da Drupal 8 quale framework evoluto, scalabile ed altamente personalizzabile

Ciò non toglie che con un po' di modifiche, commenti ed accorgimenti vari il codice in questione non possa diventare un giorno parte di un progetto più ampio, comunque funzionale o alla community drupal oppure ad uno specifico scopo personale.

 Ora tocca a voi! Date le svariate possibilità offerte dall'SDK che abbiamo utilizzato (vi invito ad approfondire al seguente indirizzo) potreste provare ad estendere quanto fatto ed ampliare quindi le funzionalità del nostro modulo custom. 

Grazie per l'attenzione!

Potrebbe interessarti: