Bots de conversa de Telegram amb Google Apps Script

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

Entrada seqüencial de paràmetres

Quan una comanda de Telegram necessita diversos paràmetres es pot optar per posar-los tots, un darrere l'altre, a la línia d'entrada. Però fent-ho així és fàcil que l'usuari cometi algun error en l'ordre, la separació o la sintaxi dels paràmetres.

Quan creem un nou bot amb el BotFather ens trobem que ens va demanar els paràmetres de manera seqüencial, un darrere l'altre. Això evita equivocar-se en l'ordre dels paràmetres i facilita informar l'usuari sobre com ha d'escriure cada paràmetre. No sabem com ho fa el BotFather, però podem intentar imitar-lo. A continuació explicarem dues maneres de fer-ho

Mètode basat en un full de càlcul

Un mètode, que hem posat en pràctica en aquest exemple, es basa en fer servir un full de càlcul per anar guardant els paràmetres que envia cada usuari. La primera columna del full de càlcul conté l'identificador de l'usuari, la segona conté el nombre de paràmetres que ha enviat per a la comanda en curs i després hi ha tantes columnes com paràmetres s'han de rebre. Quan un usuari envia la comanda, es mira si ja surt al full de càlcul o no (seria la primera vegada que fa servir la comanda); si no hi és, se l'afegeix. En qualsevol dels dos casos, es posa el comptador de paràmetres a zero i se li demana el primer paràmetre. Quan arriba un paràmetre, es guarda, s'incrementa el comptador i se li demana el següent. Quan arriba el darrer paràmetre s'executa la comanda, s'esborren els paràmetres i es torna a posar el comptador a zero.

Quan s'executa la comanda es posa el comptador a zero, en lloc d'esborrar l'usuari. Si s'esborrés l'usuari podríem tenir problemes. Imaginem-nos que hi ha dos usuaris fent servir la comanda simultàniament. Hi podria haver problemes si quan un usuari acaba s'esborrés la seva línia al full de càlcul i les fileres dels altres usuaris canviessin de posició a mig processar l'arribada d'un paràmetre.

Pot passar que l'usuari torna a enviar la comanda abans d'haver-la acabat de processar. En aquest cas, hem considerat que vol tornar a començar i ignorem els paràmetres rebuts i posem a zero el comptador.

Per implementar el sistema, hem creat tres funcions. Una prepara la filera del full de càlcul quan es rep la comanda, una és la que processa les comandes i la tercera neteja les dades quan ja s'ha processat la comanda.

La funció que prepara el full de càlcul comença cercant l'usuari, si el troba es guarda la posició on està. Tant si hi és com si no, posa el comptador a zero i deixa buides les caselles dels paràmetres. Després envia un missatge a l'usuari demanant el primer paràmetre.

function iniParam(id_usr){
  var sh = SpreadsheetApp.openById(ssId).getSheetByName("param");  // Obre el full de càlcul
  var dades = sh.getDataRange().getValues();  // Agafa els valors
  var dar_fil = sh.getLastRow();  // Índex de la darrera filera
  // Mirem si l'usuari ja consta a la llista
  var fila = 0;
  for(var i = 0; i < dar_fil; i++){  // Recorrem totes les fileres
    var row = dades[i];  /7 Agafa una filera
    var num_usuari = row[0];  // Número d'usuari en la filera actual
    if(num_usuari == id_usr){  // Mira si és el que cerquem
      fila = i + 1;  // Cal sumar 1 perquè comença a comptar des d'1
    }
  }
  // Si l'usuari és nou el donarem d'alta
  // Si no és nou, esborrarem les dades perquè entenem que vol tornar a començar
  var camps = new Array(7);  // Contindrà els valors per guardar a la taula
  camps[0] = id_usr;  // Identificador de l'usuari
  camps[1] = 0;  // Encara no tenim cap paràmetre
  for(var i = 2; i < 7; i++){
    camps[i] = "";  // Esborra tots els altres paràmetres
  }
  var valu = new Array(1);  // Matriu de dades a escriure
  valu[0] = camps;  // Converteix el vector en una matriu d'una filera i set columnes
  if(fila == 0){  // Si l'usuari és nou
    sh.appendRow(camps);   // Afegeix una filera amb les dades 
  } else {  // Si no és nou, sobrescrivim
    // 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, en el nostre cas la que hem trobat abans
    // El segon paràmetre és la columna on comença el grup, en el nostre exemple la primera (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 set
    var rang = sh.getRange(fila, 1, 1, 7);
    rang.setValues(valu);  // Guarda els valors a les caselles, substituïnt els anteriors
  }
  sendText(id_usr,missatges[0]);  // Envia el corresponent missatge a l'usuari
}

Quan l'usuari envia un text que no porta la / al davant, s'interpreta que és un paràmetre i es passa a la segona funció. El primer que fa la funció és tornar a cercar l'usuari al full de càlcul. Si no hi és, vol dir que no està enviant cap paràmetre i se li diu que la comanda és desconeguda. Si surt al full, es mira el valor del comptador de paràmetres. Si encara no és el darrer paràmetre, s'afegeix a la casella corresponent i s'incrementa el comptador. Si encara no tenim tots els paràmetres li demana el següent. Si ja tenim tots els paràmetres, es crida a la funció que processa la comanda (reserva, en aquest cas); aquesta funció cridarà a la funció que torna a deixar el full de càlcul en la situació inicial.

En aquest cas, guardem només el paràmetre però es podria afegir que es comprovés que el paràmetre rebut es correspon amb el que s'està esperant en cada moment.

Un detall important a tenir en compte és com es guarden els paràmetres al full de càlcul. Si no hem configurat les caselles amb un format determinat, el full de càlcul interpreta el que rep i ens podem trobar sorpreses. Per exemple, quan enviem 1-1 (el nom de l'aula) ho pot interpretar com 1 de gener i guardar-ho en format de data; això faria que la creació de l'esdeveniment al calendari fallés. Podríem resoldre-ho configurant cada casella amb el tipus de dades que ha de rebre però és fàcil equivocar-se o oblidar-se'n i sempre hi poden haver situacions no previstes. En lloc d'això, hem agafat una solució alternativa que ens permet prescindir de configurar els formats al full de càlcul. El que fem és guardar els paràmetres (en el cas de l'identificador de l'usuari i el comptador no cal) amb un caràcter al davant que faci que el full de càlcul ho interpreti com a text. Cal evitar els caràcters que poden fer que s'equivoqui (%, $, €, etc.) i els que és possible que formin part dels paràmetres (lletres i alguns símbols). Nosaltres hem fet servir ^. Abans d'enviar les dades a la funció que executa la comanda, ens hem de recordar de retirar aquest símbol de cada paràmetre.

function param(id_usr, comand){
  var sh = SpreadsheetApp.openById(ssId).getSheetByName("param");   // Agafa el full de càlcul
  var dades = sh.getDataRange().getValues();  // llegeix els valors
  var dar_fil = sh.getLastRow();  // Índex de la darrera filera
  // Mirem si l'usuari consta a la llista
  var fila = 0;
  for(var i = 0; i < dar_fil; i++){  // Mira totes les dades
    var row = dades[i];  // Agafa una filera
    var num_usuari = row[0];  // Identificador d'usuari de la filera actual
    if(num_usuari == id_usr){  // Mira si és el que busquem
      fila = i;  // No sumem 1 perquè al vector es compta a partir de 0
    }
  }
  if(fila == 0){  // Si l'usuari és nou, no ens està enviant cap paràmetre
    var resposta = "Comanda desconeguda";
    sendText(id_usr,resposta);
    return;
  }
  var row = dades[fila];  // Agafa la filera de l'usuari
  // A la segona casella tenim l'estat de l'enviament de paràmetres
  var estat = row[1];
  if (estat < 5){  // Si encara no hem rebut tots els paràmetres
    // Sumem 1 a la fila perquè els rangs van a partir d'1
    var rang = sh.getRange(fila+1, 2);  // Selecciona la casella de l'estat
    rang.setValue(estat + 1);  // Passem a l'estat següent
    var rang = sh.getRange(fila+1, estat+3);  // Selecciona la casella següent
    // Posem ^ al davant perquè ho interpreti com a text
    rang.setValue("^" + comand);  // Guarda el valor a la casella
  }
  if (estat == 4){  // Si ja tenim tots els paràmetres
    row[6] = "^" + comand;  // Ho afegim aquí per no tornar a llegir del full
    // Abans d'enviar-ho al calendari retirem els ^
    for (var i = 2; i < row.length; i++){
      row[i] = row[i].replace("^","");
    }
    reserva(id_usr, row, fila);  // Executa la comanda
  } else {
    sendText(id_usr,missatges[estat + 1]);
  }
}

La tercera funció és força senzilla. Només torna a posar a zero el comptador i elimina els paràmetres rebuts. Des del punt de vista del funcionament del programa, no seria necessari esborrar els paràmetres però ens pot ser útil si en algun moment hem d'entrar manualment a fer alguna comprovació al full de càlcul.

function fiParam(id_usr,fila){
  var sh = SpreadsheetApp.openById(ssId).getSheetByName("param");  // Obre el full de càlcul
  // Tornem l'usuari a l'estat inicial
  var camps = new Array(7);  // Contindrà els valors per guardar a la taula
  camps[0] = id_usr;  // Identificador de l'usuari
  camps[1] = 0;  // Encara no tenim cap paràmetre
  for(var i = 2; i < 7; i++){
    camps[i] = "";  // Esborra tots els paràmetres
  }
  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
  var rang = sh.getRange(fila+1, 1, 1, 7);  // Defineix el rang de caselles a escriure
  rang.setValues(valu);  // Guarda els valors a les caselles, substituïnt els anteriors
}

Mètode basat en la memòria cau

Un mètode, que hem posat en pràctica en aquest exemple, es basa en fer servir la memòria cau per anar guardant els paràmetres que envia cada usuari. Amb aquest mètode l'script és més curt i senzill, a més s'executa de manera més ràpida. Però hi ha el risc de perdre les dades ja que Google no en garanteix la persistència. Hem de guardar diversos paràmetres per a cada possible usuari i hem de preveure que hi hagi dos o més usuaris fent ús simultàniament del bot. Si la memòria cau permetés guardar correctament vectors o objectes podríem posar totes les dades en una d'aquestes estructures de dades i guardar-la sencera sota la clau corresponent a l'identificador de l'usuari de Telegram. Atès que això no és possible, farem servir diverses claus per a cada usuari. Cada clau serà el resultat d'afegir una lletra (corresponent al valor guardat) al final de l'identificador de l'usuari. Quan comenci el procés de recollida de paràmetres crearem les claus i les anirem omplint a mesura que avanci el procés. A l'acabar la comanda esborrarem totes les claus d'aquell usuari. Si no definim el temps màxim en el que es guarden les dades, en principi serà de deu minuts. Quan un usuari envia la comanda es crea el registre corresponent a la memòria cau, es posa el comptador de paràmetres a zero i se li demana el primer paràmetre. Quan arriba un paràmetre, es guarda, s'incrementa el comptador i se li demana el següent. Quan arriba el darrer paràmetre s'executa la comanda i s'esborren les dades guardades.

Pot passar que l'usuari torna a enviar la comanda abans d'haver-la acabat de processar. En aquest cas, hem considerat que vol tornar a començar i ignorem els paràmetres rebuts i posem a zero el comptador.

Per implementar el sistema, hem creat tres funcions. Una crea el registre a la memòria cau quan es rep la comanda, una és la que processa les comandes i la tercera neteja les dades quan ja s'ha processat la comanda.

La funció que prepara el registre guardarà l'índex 0 (cap paràmetre rebut) i posarà a zero els altres registres. El vector ind és el que guarda els indicadors dels diferents registres. L'indicador (un caràcter) s'afegeix al final de l'identificador d'usuari per crear les diferents claus. Després envia un missatge a l'usuari demanant el primer paràmetre.

function iniParam(id_usr){
  var cache = CacheService.getScriptCache();  // Llegeix la memòria cau
  // Si l'usuari és nou el donarem d'alta
  // Si no és nou, esborrarem les dades perquè entenem que vol tornar a començar
  cache.put(id_usr+ind[0], 0);  // Nombre de paràmetres
  for(var i = 1; i < ind.length; i++){
    cache.put(id_usr+ind[i], "");  // Paràmetres
  }
  sendText(id_usr,missatges[0]);
}

Quan l'usuari envia un text que no porta la / al davant, s'interpreta que és un paràmetre i es passa a la segona funció. El primer que fa la funció és llegir les dades a la memòria cau. Si l'usuari no hi és, vol dir que no està enviant cap paràmetre i se li diu que la comanda és desconeguda. Si hi ha dades, es mira el valor del comptador de paràmetres. Si encara no és el darrer paràmetre, s'afegeix a la clau corresponent i s'incrementa el comptador. Si encara no tenim tots els paràmetres li demana el següent. Si ja tenim tots els paràmetres, es crida a la funció que processa la comanda (reserva, en aquest cas); aquesta funció, quan hagi acabat, cridarà a la que esborra les dades rebudes.

En aquest cas, guardem només el paràmetre però es podria afegir que es comprovés que el paràmetre rebut es correspon amb el que s'està esperant en cada moment. El valor de la variable pas pot ser considerat text i caldrà convertir-lo a numèric.

function param(id_usr, comand){
  var cache = CacheService.getScriptCache();  // Llegeix la memòria cau
  var pas = cache.get(id_usr+ind[0]);  // Nombre de paràmetres rebuts
  // Mirem si l'usuari consta a la llista
  if(pas == null){  // Si l'usuari és nou, no ens està enviant cap paràmetre
    var resposta = "Comanda desconeguda";
    sendText(id_usr,resposta);
    return;
  }
  var estat = +pas;  // Passem al pas següent
  if (estat < 5){  // Si encara no hem rebut tots els paràmetres
    cache.put(id_usr+ind[0], estat+1);  // Guarda el nou estat
    cache.put(id_usr+ind[estat+1], comand);  // Les dades es guarden, en principi, durant deu minuts
  }
  if (estat == 4){  // Si ja tenim tots els paràmetres
    reserva(id_usr);  // Executa la comanda
  } else {
    sendText(id_usr,missatges[estat+1]);  // Demana el paràmetre següent
  }
}

La tercera funció és molt senzilla. Només esborra de la memòria cau les dades corresponents a l'usuari.

function fiParam(id_usr){
  var cache = CacheService.getScriptCache();  // Llegeix la memòria cau
  for(var i = 0; i < ind.length; i++){
    cache.remove(id_usr+ind[i]);  // Esborra les dades
  }
}

 

 

 

 

 

 

 

 

 

 

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