Bots de conversa de Telegram amb Google Apps Script

Bots de conversa Exemples Dades pràctiques   Recursos CITCEA
Google Apps Script Projectes Interacció   Inici

Gestionem les inscripcions a uns actes

En aquest exemple gestionarem les inscripcions a un o més actes. Hi haurà una llista dels actes existents en la que s'hi indicarà si tenen les inscripcions obertes o no. Cada acte tindrà un identificador.

Quan un usuari demani la inscripció a un acte, s'anotarà la informació i se li enviarà un correu electrònic perquè confirmi la inscripció. Un cop confirmada la inscripció, l'usuari es podrà baixar un codi QR que serà la seva entrada a l'esdeveniment.

La taula dels actes disponibles (anomenada actes) tindrà, com a mínim, les següents columnes:

Columna Contingut Exemple o valors
0 Identificador de l'acte
(no ha de tenir espais)
gen2004
1 Estat de l'acte 0     No admet inscripcions
1     Admet inscripcions

La taula de les inscripcions (anomenada inscrip) tindrà, com a mínim, les següents columnes:

Columna Contingut Exemple o valors
0 Identificador de l'acte
(no ha de tenir espais)
gen2004
1 Identificador de Telegram de l'usuari inscrit ^^9499826
2 Data d'inscripció 09/12/2003 18:15:29
3 Correu electrònic de l'usuari inscrit usuari@correu.com
4 Estat de la inscripció 0     Inscripció no validada
1     Inscripció validada
2     La persona ja ha entrat a l'acte
5 Codi de seguretat WDApRdzlla5VXMlIbZaD

L'script fa servir POST per interaccionar amb Telegram i GET per validar la inscripció (des d'un enllaç en el correu) i per autoritzar l'entrada (des d'un enllaç en un codi QR). Els paràmetres disponibles a POST són:

Paràmetre Descripció Exemple
/help Llista de comandes disponibles /help
/inscri idActe correu Inscripció a un acte concret /inscri gen2004 usuari@correu.com
/qr@idActe Obtenir el codi QR per entrar a un acte concret /qr@gen2004

Quan es demana fer la inscripció, es comprova que l'usuari hagi entrat el codi de l'esdeveniement i una adreça de correu mínimament acceptable. No és possible comprovar si l'adreça és vàlida i, per tant, comprovem que tingui una arroba i que hi hagi almenys un punt després de l'arroba. Abans i després dels símbols comprovats hi ha d'haver un mínim de dos caràcters. Cal tenir en compte que hi pot haver adreces que tinguin un o més punts abans de l'arroba i també hi ha terminacions de més de tres lletres. En el cas de la petició de codi QR es comprova que s'hagi indicat l'acte.

A l'hora de fer la inscripció, primer es comprova que l'acte corresponent estigui enregistrat i accepti inscripcions. Després es mira si l'usuari ja està inscrit i, si és el cas, se li diu quan es va inscriure. Si no ho està, es guarden les seves dades i se li envia un correu perquè validi la inscripció. Per evitar problemes amb alguns programes de gestió de correu, s'escurça l'adreça de l'enllaç.

A l'hora d'enviar el codi QR, primer es comprova que l'usuari estigui correctament inscrit a l'acte per al qual demana el codi i que encara no ha entrat. Si tot és correcte, es genera el codi i s'envia.

Per donar una mica de seguretat, en el moment d'inscriure un usuari a un acte es genera un codi alfanumèric de setze caràcters ASCII que es guarda al full de càlcul i s'envia codificat en l'enllaç de correu per poder fer la comprovació en la validació de la inscripció. Quan es valida la inscripció es genera un nou codi alfanumèric, aquest cop de vint caràcters, que formarà part de l'enllaç que es codifica en el codi QR i serà comprovat a l'hora d'entrar.

Quan es valida la inscripció es comprova que l'acte i el codi que figuren a la petició GET corresponen amb una de les peticions existents. No s'envia l'identificador de Telegram per protegir el compte de l'usuari. Una cosa semblant es fa a la validació del codi QR per entrar. El valor de la variable posicio pot ser considerat text i caldrà convertir-lo a numèric.

Atès que l'adreça del generador de codis té paràmetres, pot aparèixer un conflicte si l'adreça que volem codificar conté algun & (o sigui, si també té paràmetres). Caldrà substituir els & de l'adreça que es vol codificar pel seu codi per a http, és a dir %26.

El programa de l'script és el següent:

var token = "^^34328844:AAFIpk-e7j3UZtYQYQaTduf4hEhnDqIcNXI"; // API Token de Telegram 
var telegramUrl = "https://api.telegram.org/bot" + token;  // URL que comunica el bot de conversa amb Telegram 
var ssId = "^^ThsoSjkeMSfwEKy4mn_4QEYH96sxv3VURqE3WHCTswDA";  // Identificador del full de càlcul
// Adreça URL de l'script
var webAppUrl = "https://script.google.com/macros/s/^^fycbzXSTtXkzqyM_sABbEZanVX2EuRAIUB5xct53AXp6-MbLscpLHO/exec";
function doGet(e){  // Per a l'enllaç del correu i per al codi QR
  var resposta = "";
  // Agafem els paràmetres de l'ordre GET i comprovem que hi ha el contingut esperat
  var Accio = e.parameter.accio;
  var Codi = e.parameter.codi;
  var Acte = e.parameter.acte;
  if ((Accio == null) || (Accio == undefined)){
    resposta = "Falten paràmetres";
  } else {
    resposta = "Comanda incorrecta";  // Frase per defecte
    if (Accio == "ins"){  // Correspon a l'enllaç del correu
      if ((Codi == null) || (Codi == undefined) || (Acte == null) || (Acte == undefined)){
        resposta = "Falten paràmetres";
      } else {
        var sh = SpreadsheetApp.openById(ssId).getSheetByName("inscrip");  // Agafem el full
        var dades = sh.getDataRange().getValues();  // Agafem les dades per buscar l'usuari
        var posicio = 0;  // De moment, no l'hem trobat
        var id = "";  // Si el trobem, agafem l'identificador de l'usuari
        resposta = "Petició incorrecta";  // Per si no el trobem
        for (var i in dades){
          if((posicio == 0) && (i > 0)){  // Si ja l'hem trobat, no seguim
            var filera = dades[i];  // Agafa una filera
            if((filera[0] == Acte) && (filera[5] == Codi)){  // L'hem trobat
              if(filera[4] < 1){  // Comprovem que està pendent de validar
                posicio = i;  // Filera de l'usuari
                id = filera[1];  // Identificador de l'usuari
              }
            }
          }
        }
        posicio = +posicio + 1;  // Sumem 1 perquè a l'escriure es compta des d'1
        if(posicio > 1){  // Si hem trobat l'usuari
          var camps = new Array(2);  // Valors per guardar a la taula
          camps[0] = 1;  // La inscripció està validada
          camps[1] = codi(20);  // Posem un nou codi, de mida diferent
          // Ja tenim les dues caselles que hem d'escriure a la filera
          // Les nostres dades han d'escriure en un espai, que seleccionarem més endavant, 
          //    de dues columnes en una filera
          var valu = new Array(1);  // Matriu de dades a escriure
          valu[0] = camps;  // Converteix el vector en una matriu d'una filera i dues columnes
          // La funció getRange ens permet seleccionar el grup de caselles sobre les que anem a escriure
          // El primer paràmetre és la filera on comença el grup
          // El segon paràmetre és la columna on comença el grup, en el nostre cas la cinquena (comencen en 1)
          // El tercer paràmetre és el nombre de fileres que tindrà el grup, per a nosaltres una
          // El quart paràmetre és el nombre de columnes que ocupa el grup, en el nostre cas dues
          var rangeVal = sh.getRange(posicio, 5, 1, 2);  // Selecciona dues caselles
          rangeVal.setValues(valu);  // Guarda els valors a les caselles, substituïnt els anteriors
          resposta = "Inscripció validada";
          // La resposta sortirà al navegador i s'enviarà a Telegram
          // També podríem enviar un correu electrònic
          // Podríem enviar (per correu o per Telegram) un enllaç per baixar la documentació
          var resp = resposta + "\nPer entrar a l'acte hauràs de mostrar un codi QR que ";
          resp = resp + "obtindràs amb la comanda /qr@" + Acte;
          sendText(id,resp);
        }
      }
    }
    if (Accio == "ent"){  // Per quan es valida el codi QR
      if ((Codi == null) || (Codi == undefined) || (Acte == null) || (Acte == undefined)){
        resposta = "Falten paràmetres";
      } else {
        var sh = SpreadsheetApp.openById(ssId).getSheetByName("inscrip");  // Agafem el full
        var dades = sh.getDataRange().getValues();  // Agafem les dades
        var posicio = 0;  // De moment, no l'hem trobat
        var estat = -1;  // Aquí guardarem l'estat de l'usuari
        resposta = "Petició incorrecta";  // Per si no el trobem
        for (var i in dades){  // Mirem totes les dades
          if((posicio == 0) && (i > 0)){  // Si ja l'hem trobat, no seguim
            var filera = dades[i];  // Agafa una filera
            if((filera[0] == Acte) && (filera[5] == Codi)){  // L'hem trobat
              estat = filera[4];  // Agafem el seu estat
              posicio = i;  // I la seva posició
            }
          }
        }
        posicio = +posicio + 1;  // Sumem 1 perquè a l'escriure es compta des d'1
        if(posicio > 1){  // Si l'hem trobat
          if(estat == 0){
            resposta = "Aquesta persona no ha validat la inscripció";
          }
          if(estat == 2){
            resposta = "Aquesta persona ja ha entrat";
          }
          if(estat == 1){
            // La funció getRange ens permet seleccionar la casella sobre la que anem a escriure
            // El primer paràmetre és la filera
            // El segon paràmetre és la casella
            var rangeVal = sh.getRange(posicio, 5);  // Selecciona la casella
            rangeVal.setValue(2);  // Guarda el valor
            resposta = "Tot correcte. Pot passar!";
          }
        } else {
          resposta = "Aquesta persona no està inscrita";
        }
      }
    }
  }
  return ContentService.createTextOutput(resposta);
}
function doPost(e){  // Per a les comandes de Telegram
  var data = JSON.parse(e.postData.contents);  // Llegeix les dades rebudes per JSON i les guarda
  var text = data.message.text;  // Comanda enviada
  var id = data.message.chat.id;  // Identificador de la finestra d'on prové el missatge 
  var id_usuari = data.message.from.id; // Identificador de l'usuari
  var id_missatge = data.message.message_id; // Identificador del missatge
  var lang = data.message.from.language_code ;  // Idioma de l'usuari 
  var nom_usr = data.message.from.first_name ;  // Nom de l'usuari
  var location = data.message.location;  // Localització de l'usuari (si es sap) 
  var realitzat = false;
  // En la comanda /qr hem posat @ per conservar l'enllaç
  // Amb qualsevol altre separador, Telegram talla
  text = text.replace("/qr@","/qr ");  // Substituïm així per conservar l'adreça de correu
  var comanda = text.split(" ");  // Separem els paràmetres 
  var cmd = comanda[0];  // Comanda
  var par = comanda[1];  // Identificador de l'acte, no pot tenir espais
  var mail = comanda[2];  // Correu de l'usuari
  if(cmd == '/inscri'){  // Inscripció a l'acte
    var correcte = true;
    var resposta = "";
    if(par == undefined){
      resposta = resposta + "Has d'indicar a quin acte et vols inscriure i el teu correu\n";
      resposta = resposta + "/inscri idActe correu\n";
      correcte = false;
    }
    if(mail == undefined){
      resposta = resposta + "Has d'indicar a quin acte et vols inscriure i el teu correu\n";
      resposta = resposta + "/inscri idActe correu\n";
      correcte = false;
    }
    if(correcte){  // Comprovem que sigui una adreça acceptable
      correcte = comprova(mail);
    }
    if(correcte){
      resposta = resposta + inscrip(par, id_usuari, mail);  // Fa la inscripció
    }
    sendText(id,resposta);        
    realitzat = true;
  }
  if(cmd == '/qr'){  // Petició del QR
    var correcte = true;
    var resposta = "";
    if(par == undefined){
      resposta = resposta + "Has d'indicar a quin acte vols entrar\n";
      resposta = resposta + "/qr@idActe\n";
      correcte = false;
    }
    if(correcte){
      correcte = envia_qr(par, id_usuari);  // Envia el QR
    } else {
      sendText(id,resposta);
    }
    if (!correcte){
      var resposta = "No estàs inscrit a " + par;
      sendText(id,resposta);
    }
    realitzat = true;
  }
  if(cmd == '/help'){  // Llista de comandes
    var resposta = "Comandes disponibles:\n";
    resposta = resposta + "/inscri idActe   Inscripció a l'acte indicat\n";
    resposta = resposta + "/qr@idActe Descarregar el codi QR necessari per entrar a l'acte\n";
    sendText(id,resposta);  
    realitzat = true;
  }
  if (!realitzat){
    var resposta = "Comanda desconeguda";
    sendText(id,resposta);
  }
}
function envia_qr(acte, id_usr){  // Funció que envia el codi QR per entrar
  var sh = SpreadsheetApp.openById(ssId).getSheetByName("inscrip");  // Agafem el full
  var dades = sh.getDataRange().getValues();  // Agafem les dades
  var posicio = 0;  // De moment, no l'hem trobat
  var codi = "";  // Si el trobem, agafem el codi
  resposta = "Petició incorrecta";  // Per si no el trobem
  for (var i in dades){  // Recorre totes les dades
    if((posicio == 0) && (i > 0)){  // Si ja l'hem trobat, no seguim
      var filera = dades[i];  // Agafa una filera
      if((filera[0] == acte) && (filera[1] == id_usr)){  // L'hem trobat
        if(filera[4] < 2){  // Comprovem que encara no ha entrat
          posicio = i;  // Filera que correspon a l'usuari
          codi = filera[5];  // Identificador de l'usuari
        }
      }
    }
  }
  if(posicio > 0){  // Si l'hem trobat
    var  caption = "Entrada a " + acte; // Missatge que acompanya al codi 
    // Quan hem de fer servir & com a paràmetre d'un paràmetre, com en aquest cas, l'hem de codificar
    // sinó no ho interpreta correctament 
    var codiAnd = "%26";  // Codi de substitució per al signe &
	// Els signes & de l'adreça de l'script s'han de canviar perquè no es confonguin 
	// amb els paràmetres de l'adreça del generador de codis QR
    // scriptUrl seria l'adreça d'un script que validaria el codi  
    var url_script = webAppUrl +"?accio=ent" + codiAnd + "acte=" + acte + codiAnd + "codi=" + codi; 
    var qr = "https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=" + url_script; 
    var response = UrlFetchApp.fetch(qr); // Carrega la imatge corresponent a la url generada 
    var binaryData = response.getContent();  // La guarda com a dades binàries
    // La funció newBlob té tres paràmetres: les dades, el tipus de dades i el nom que li volem donar
    // carrega la imatge com a dades binaries en una variable
    var blob = Utilities.newBlob(binaryData, 'image/jpeg', 'QR');  // Dades BLOB de la imatge
    sendBlobFile(id_usr,blob,caption); // Enviem la imatge al Telegram en format binari (BLOB) 
    return true;                      
  } else {
    return false;
  }
}
function inscrip(acte, id_usr, correu){  // Funció que fa la inscripció a un acte
  // Primer mirem si l'acte demanat existeix i té inscripcions obertes
  var sh = SpreadsheetApp.openById(ssId).getSheetByName("actes");  // Agafem el full
  var dades = sh.getDataRange().getValues();  // Agafa les dades
  var obert = false;  // L'acte no té, d'entrada, inscripció oberta
  for (var i in dades){  // Recorre totes les dades
    if(i > 0){  // Ignorem la filera dels títols
      var filera = dades[i];  // Agafa una filera
      if(filera[0] == acte){  // Hem trobat l'acte demanat
        if(filera[1] > 0){  // L'acte té inscripcions obertes
          obert = true;
        }
      }
    }
  }
  if (obert){
    // Comprovem que l'usuari no estigui ja inscrit
    var sh = SpreadsheetApp.openById(ssId).getSheetByName("inscrip");  // Agafem el full
    var dades = sh.getDataRange().getValues();  // Agafem les dades
    var present = false;  // L'usuari, en principi, no està inscrit
    for (var i in dades){  // Recorre totes les dades
      if(i > 0){  // Ignorem la filera dels títols
        var filera = dades[i];  // Agafa una filera
        if((filera[0] == acte) && (filera[1] == id_usr)){  // L'hem trobat
          present = true;
          var data_ins = filera[2];  // Data en la qual es va inscriure
        }
      }
    }
    if(present){
      var resp = "Ja et vas inscriure el dia ";
      resp = resp + diahora(data_ins);
    } else {
      var camps = new Array(6);  // Contindrà cinc valors per guardar a la taula
      camps[0] = acte;  // Acte al qual es fa la inscripció
      camps[1] = id_usr;  // Usuari que s'inscriu
      camps[2] = new Date;  // Data en la que s'ha fet la inscripció
      camps[3] = correu;  // Correu de l'usuari
      camps[4] = 0;  // De moment, inscripció no validada
      camps[5] = codi(16);  // Codi aleatori de seguretat
      var sh = SpreadsheetApp.openById(ssId).getSheetByName("inscrip");  // Agafem el full
      sh.appendRow(camps);   // Afegeix una filera amb les dades 
	  // Crea la URL escurçada
      var url = escur(webAppUrl + "?accio=ins&acte=" + acte + "&codi=" + camps[5]);
      var titol = "Inscripció a " + acte;  // Títol del correu
      var text = "Benvolgut/da\n\n";  // Text del correu
      text = text + "Hem rebut la teva petició d'inscripció a " + acte +".\n\n";
      text = text + "Per completar el procés, cal que segueixis el següent enllaç:\n";
      text = text + url + "\n\nMoltes gràcies\n\nL'equip organitzador";
      MailApp.sendEmail(correu, titol, text);  // Envia el correu
      var resp = "Inscripció realitzada\n";  // Resposta que s'envia a Telegram
      resp = resp + "Rebràs un correu amb les instruccions per confirmar la inscripció";
    }
  } else {
    var resp = "L'acte demanat no té inscripcions obertes";
  }
  return resp;
}
function comprova(adre){
  // Mira si l'adreça de correu compleix uns mínims
  // no es pot saber si és correcta sense provar-la
  // Comprova que, com a mínim, hi hagi una arroba i un punt
  // Un mínim de dos caràcters abans de l'arroba
  // Un mínim de dos caràcters a cada banda del punt
  var resp = true;  // D'entrada, suposem que és una adreça vàlida
  var camps = adre.split("@");  // Separem per l'arroba
  if(camps[0].length < 2){
    resp = false;  // No és vàlid, pocs caràcters abans de l'@
  }
  if(camps[1] == undefined){  // No hi havia res després de l'arroba
    resp = false;
  } else {
    var serv = camps[1].split(".");  // Separem pel punt
    if(serv[0].length < 2){
      resp = false;  // No és vàlid, pocs caràcters abans del punt
    }
    if(serv[1] == undefined){  // No hi havia res després del punt
      resp = false;
    } else {
      if(serv[1].length < 2){
        resp = false;  // No és vàlid, pocs caràcters després del punt
      }
    }
  }
  return resp;
}
function codi(n){  // Funció que genera un codi aleatori de n caràcters
  // El codi es genera a partir de xifres (48 a 57), lletres majúscules (65 a 90) 
  //   i lletres minúscules (97 a 122); en total 62 caràcters possibles
  // De 0 a 9 és una xifra
  // de 10 a 35 una lletra majúscula
  // de 36 a 61 una lletra minúscula
  var resp = "";
  for (var i = 0; i < n; i++){
    var aleat = Math.floor(62 * Math.random());  // Valor enter entre 0 i 61
    var asc = 65 + aleat - 10;  // Per si està entre 10 i 35, majúscules
    if (aleat < 10){
      asc = 48 + aleat;  // Correspon a un dígit
    }
    if (aleat > 35){
      asc = 97 + aleat - 10 - 26;  // Minúscules
    }
    var carac = String.fromCharCode(asc);  // Agafa el caràcter corresponent al codi ASCII
    resp = resp + carac;
  }
  return resp;
}
function diahora(data){  // Funció que converteix la data al format d'ús habitual
  var resp = data.getDate();
  resp = resp + "-" + (data.getMonth() + 1);  // Els mesos comencen en zero
  resp = resp + "-" + data.getFullYear();
  resp = resp + " a les " + data.getHours();
  resp = resp + "." + data.getMinutes();
  resp = resp + " h";
  return resp;
}
function escur(url_llarg){  // Escurça una adreça URL
  // Basada en una adaptació feta per Ferran Mas (@FerranMas) d'una funció 
  // original de @pfelipm 
  var adre = "https://tinyurl.com/api-create.php?url=" + url_llarg; 
  var url_curt = UrlFetchApp.fetch(adre).getContentText(); 
  return url_curt; 
}
function sendText(chatId,text_env,keyBoard){  // Funció que envia un text o un teclat a Telegram 
  keyBoard = keyBoard || 0;
  if(keyBoard.inline_keyboard || keyBoard.keyboard){
    var data = {
      method: "post",
      payload: {
        method: "sendMessage",
        chat_id: String(chatId),
        text: text_env,
        parse_mode: "HTML",
        reply_markup: JSON.stringify(keyBoard)
      }
    }
  } else {
    var data = {
      method: "post",
      payload: {
        method: "sendMessage",
        chat_id: String(chatId),
        text: text_env,
        parse_mode: "HTML"
      }
    }
  }
  UrlFetchApp.fetch( telegramUrl + '/', data);
}
function sendBlobFile(chatId,blob_data,caption){
  var payload = {
    method: "sendPhoto",
    chat_id: String(chatId),
    photo: blob_data,
    caption: caption,
    parse_mode: "HTML"
  };
  var options = {
    method: "POST",
    payload: payload,
    muteHttpExceptions: true
  };
  UrlFetchApp.fetch( telegramUrl + '/', options);
}

Atenció: Se suposa que quan es llegeix el codi QR el dispositiu obre el navegador i carrega la pàgina web que correspon a l'enllaç, ja sigui directament o després de prémer un botó. Però hem detectat que alguns lectors de codi QR executen l'enllaç abans que l'usuari pugui dir si carrega la pàgina o no. Atès que no carrega la pàgina no mostra la resposta però, en canvi, sí queda enregistrat l'accés. Quan l'usuari accedeix a que es carregui la pàgina es torna a executar l'script que donarà un avís indicant que l'usuari ja ha entrat. Una forma de solucionar aquest problema, si només hi ha una persona que valida els codis seria que la resposta a la lectura del codi s'enviés al Telegram de la persona que controla l'accés. Una altra opció seria modificar l'script per tal que l'estat pugui arribar fins a 3 i es deixi entrar quan val 2.

Aquest programa admet diferents ampliacions. Per un costat es podria afegir una comanda que permetés generar automàticament un diploma o certificat d'assistència. Podríem posar a la taula d'actes, en columnes addicionals, les dades necessàries per fer el document però també caldria recollir d'alguna manera el nom de l'usuari ja que ara només recollim el correu.

Podríem fer que quan l'usuari valida la inscripció rebés un o més enllaços, ja sigui per Telegram o per correu electrònic, que li permetessin descarregar la documentació corresponent.

En cas que preveiem que els actes puguin ser propers en el temps, com seria el cas de diversos tallers en un congrés o jornada, es podria posar una comanda que permetés a l'usuari obtenir un llistat dels actes als que està inscrit.

També es podria tenir una comanda oculta (que no es mostri en fer /help) que permetés als organitzadors descarregar un llistat de les persones inscrites. En cas que hi hagi, per exemple, diversos tallers en una jornada, es podrien tenir les dades de les persones que imparteixen els tallers i que cada una d'aquestes pogués consultar les persones inscrites en la seva activitat.

 

 

 

 

 

 

 

 

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.