Alternativa a TwitterFeed y FeedBurner con Google Apps Script

Edit el 12/2/18

Como bien sabes estoy "reviviendo" este blog, hacía cerca de mil años (año arriba, año abajo) que no publicaba nuevas entradas y me he encontrado con algunas cosas que han cambiado. Entre ellas, que TwitterFeed cerraba sus puertas en noviembre del 2016 y que FeedBurner se ha convertido en una especie de zombie del que algunos hemos huido. Con lo que en mi cuenta ¿personal? @Ivan_rg, no se estaban lanzando automáticamente los Tweets cuando publicaba un nuevo artículo en este blog. Putadilla...

Ante esto he buscado diferentes alternativas, por supuesto he pensado en las míticas Dlvr.it, IFTTT o Hootsuite, incluso en alguna otra menos conocida, pero al final me he decidido por utilizar los recursos de Google y montarme el tinglado por mi mismo. Así que hoy comparto este mini-tutorial para crear un automatizador que envíe un Tweet cada vez que haya contenido nuevo en tu blog.

  1. Crea tu propia aplicación de Twitter.

    Esta parte es muy sencilla, como usuario de Twitter, puedes crear tus propias aplicaciones para utilizar los recursos de la plataforma, así que vamos a crear una con la cuenta de Twitter que quieres que envíe los tweets (si no has creado una aplicación anteriormente).
    Logueado con la cuenta de Twitter entra en Twitter Application Management y crea una nueva aplicación:

    Una vez creada, debes autorizar tu cuenta para que utilice la aplicación:


     Ahora tan sólo debes apuntar los siguientes códigos que utilizaremos más adelante:
    • Consumer Key (API Key).
    • Consumer Secret (API Secret).
    • Access Token.
    • Access Token Secret.

  2. Trabajando con Google Apps Script.

    En tu cuenta de Google Drive crea un nuevo archivo de Google Apps Script y ponle nombre, a continuación crea un nuevo archivo de secuencia de comandos y llámalo "OAuth":


    Copia y pega en OAuth.gs el siguiente código sin realizar cambios:


    /**
     * Adds a OAuth1 object to the global scope. This can be used as follows:
     *
     * var urlFetch = OAuth1.withAccessToken(consumerKey, consumerSecret,
     *     accessToken, accessSecret);
     * var response = urlFetch.fetch(url, params, options);
     */
    (function(scope) {
      /**
       * Creates an object to provide OAuth1-based requests to API resources.
       * @param {string} consumerKey
       * @param {string} consumerSecret
       * @param {string} accessToken
       * @param {string} accessSecret
       * @constructor
       */
      function OAuth1UrlFetchApp(
          consumerKey, consumerSecret, accessToken, accessSecret) {
        this.consumerKey_ = consumerKey;
        this.consumerSecret_ = consumerSecret;
        this.accessToken_ = accessToken;
        this.accessSecret_ = accessSecret;
      }

      /**
       * Sends a signed OAuth 1.0 request.
       * @param {string} url The URL of the API resource.
       * @param {?Object.<string>=} opt_params Map of parameters for the URL.
       * @param {?Object.<string>=} opt_options Options for passing to UrlFetchApp
       *     for example, to set the method to POST, or to include a form body.
       * @return {?Object} The resulting object on success, or null if a failure.
       */
      OAuth1UrlFetchApp.prototype.fetch = function(url, opt_params, opt_options) {
        var oauthParams = {
          'oauth_consumer_key': this.consumerKey_,
          'oauth_timestamp': parseInt(new Date().getTime() / 1000),
          'oauth_nonce': this.generateNonce_(),
          'oauth_version': '1.0',
          'oauth_token': this.accessToken_,
          'oauth_signature_method': 'HMAC-SHA1'
        };

        var method = 'GET';
        if (opt_options && opt_options.method) {
          method = opt_options.method;
        }
        if (opt_options && opt_options.payload) {
          var formPayload = opt_options.payload;
        }

        var requestString =
            this.generateRequestString_(oauthParams, opt_params, formPayload);
        var signatureBaseString =
            this.generateSignatureBaseString_(method, url, requestString);
        var signature = Utilities.computeHmacSignature(
            Utilities.MacAlgorithm.HMAC_SHA_1, signatureBaseString,
            this.getSigningKey_());
        var b64signature = Utilities.base64Encode(signature);

        oauthParams['oauth_signature'] = this.escape_(b64signature);
        var fetchOptions = opt_options || {};
        fetchOptions['headers'] = {
          Authorization: this.generateAuthorizationHeader_(oauthParams)
        };
        if (fetchOptions.payload) {
          fetchOptions.payload = this.escapeForm_(fetchOptions.payload);
        }
        return UrlFetchApp.fetch(
            this.joinUrlToParams_(url, opt_params), fetchOptions);
      };

      /**
       * Concatenates request URL to parameters to form a single string.
       * @param {string} url The URL of the resource.
       * @param {?Object.<string>=} opt_params Optional key/value map of parameters.
       * @return {string} The full path built out with parameters.
       */
      OAuth1UrlFetchApp.prototype.joinUrlToParams_ = function(url, opt_params) {
        if (!opt_params) {
          return url;
        }
        var paramKeys = Object.keys(opt_params);
        var paramList = [];
        for (var i = 0, paramKey; paramKey = paramKeys[i]; i++) {
          paramList.push([paramKey, opt_params[paramKey]].join('='));
        }
        return url + '?' + paramList.join('&');
      };

      /**
       * Generates a random nonce for use in the OAuth request.
       * @return {string} A random string.
       */
      OAuth1UrlFetchApp.prototype.generateNonce_ = function() {
        return Utilities
            .base64Encode(Utilities.computeDigest(
                Utilities.DigestAlgorithm.SHA_1,
                parseInt(Math.floor(Math.random() * 10000))))
            .replace(/[\/=_+]/g, '');
      };

      /**
       * Creates a properly-formatted string from a map of key/values from a form
       * post.
       * @param {!Object.<string>} payload Map of key/values.
       * @return {string} The formatted string for the body of the POST message.
       */
      OAuth1UrlFetchApp.prototype.escapeForm_ = function(payload) {
        var escaped = [];
        var keys = Object.keys(payload);
        for (var i = 0, key; key = keys[i]; i++) {
          escaped.push([this.escape_(key), this.escape_(payload[key])].join('='));
        }
        return escaped.join('&');
      };

      /**
       * Returns a percent-escaped string for use with OAuth. Note that
       * encodeURIComponent is not sufficient for this as the Twitter API expects
       * characters such as exclamation-mark to be encoded. See:
       *     https://dev.twitter.com/discussions/12378
       * @param {string} str The string to be escaped.
       * @return {string} The escaped string.
       */
      OAuth1UrlFetchApp.prototype.escape_ = function(str) {
        return encodeURIComponent(str).replace(/[!*()']/g, function(v) {
          return '%' + v.charCodeAt().toString(16);
        });
      };

      /**
       * Generates the Authorization header using the OAuth parameters and
       * calculated signature.
       * @param {!Object} oauthParams A map of the required OAuth parameters. See:
       *     https://dev.twitter.com/oauth/overview/authorizing-requests
       * @return {string} An Authorization header value for use in HTTP requests.
       */
      OAuth1UrlFetchApp.prototype.generateAuthorizationHeader_ = function(
          oauthParams) {
        var params = [];
        var keys = Object.keys(oauthParams).sort();
        for (var i = 0, key; key = keys[i]; i++) {
          params.push(key + '="' + oauthParams[key] + '"');
        }
        return 'OAuth ' + params.join(', ');
      };

      /**
       * Generates the signature string for the request.
       * @param {string} method The HTTP method e.g. GET, POST
       * @param {string} The URL.
       * @param {string} requestString The string representing the parameters to the
       *     API call as constructed by generateRequestString.
       * @return {string} The signature base string. See:
       *     https://dev.twitter.com/oauth/overview/creating-signatures
       */
      OAuth1UrlFetchApp.prototype.generateSignatureBaseString_ = function(
          method, url, requestString) {
        return [method, this.escape_(url), this.escape_(requestString)].join('&');
      };

      /**
       * Generates the key for signing the OAuth request
       * @return {string} The signing key.
       */
      OAuth1UrlFetchApp.prototype.getSigningKey_ = function() {
        return this.escape_(this.consumerSecret_) + '&' +
            this.escape_(this.accessSecret_);
      };

      /**
       * Generates the request string for signing, as used to produce a signature
       * for the Authorization header. see:
       * https://dev.twitter.com/oauth/overview/creating-signatures
       * @param {!Object} oauthParams The required OAuth parameters for the request,
       *     see: https://dev.twitter.com/oauth/overview/authorizing-requests
       * @param {?Object=} opt_params Optional parameters specified as part of the
       *     request, in map form, for example to specify /path?a=b&c=d&e=f... etc
       * @param {?Object=} opt_formPayload Optional mapping of pairs used in a form
       *     as part of a POST request.
       * @return {string} The request string
       */
      OAuth1UrlFetchApp.prototype.generateRequestString_ = function(
          oauthParams, opt_params, opt_formPayload) {
        var requestParams = {};
        var requestPath = [];
        for (var i = 0; i < arguments.length; i++) {
          var mapping = arguments[i];
          if (mapping) {
            var paramKeys = Object.keys(mapping);
            for (var j = 0, paramKey; paramKey = paramKeys[j]; j++) {
              requestParams[paramKey] = mapping[paramKey];
            }
          }
        }
        var requestKeys = Object.keys(requestParams);
        requestKeys.sort();

        for (var m = 0, requestKey; requestKey = requestKeys[m]; m++) {
          requestPath.push([
            this.escape_(requestKey), this.escape_(requestParams[requestKey])
          ].join('='));
        }
        return requestPath.join('&');
      };

      /**
       * Builds a OAuth1UrlFetchApp object based on supplied access token (and other
       * parameters.
       * @param {string} consumerKey
       * @param {string} consumerSecret
       * @param {string} accessToken
       * @param {string} accessSecret
       * @return {!OAuth1UrlFetchApp}
       */
      function withAccessToken(
          consumerKey, consumerSecret, accessToken, accessSecret) {
        return new OAuth1UrlFetchApp(
            consumerKey, consumerSecret, accessToken, accessSecret);
      }

      scope.OAuth1 = {withAccessToken: withAccessToken};
    })(this);

    Ahora edita el archivo Código.gs y copia y pega en él el siguiente código:


    function LanzaBot(){
      //AÑADE AQUÍ LA DIRECCIÓN URL DEL FEED TU BLOG
      var res = UrlFetchApp.fetch('http://www.twittboy.com/feeds/posts/default?alt=rss').getContentText(); 
      var result = revisaFeed(res);
      return;
    }
    var revisaFeed = function(res){
      var document = XmlService.parse(res);
      var root = document.getRootElement();
      var items = document.getRootElement().getChild('channel').getChildren('item');
        try {   
      var titulo =   items[0].getChild('title').getText();
      var link =   items[0].getChild('link').getText();
      var props = PropertiesService.getScriptProperties();
      var desdeID = props.getProperty("DESDE_POST_ID"); 
      var cero = Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD2, items[0].getChild('guid').getText()));   
      props.setProperty("DESDE_POST_ID", cero);
      if (desdeID==cero) {
        Logger.log("IDs Iguales, último twitteado: " + titulo + " Enlace: " + link );
      } else {
        Logger.log("NUEVO TWITTEADO!!: " + titulo + " Enlace: " + link ); 
        // ELIGE EL TEXTO QUE PRECEDE AL TÍTULO O DÉJALO VACÍO
      var tweet =  'Nuevo artículo en mi blog! ' + titulo + ' ' + link; 
      mandaTweet(tweet);
      }
     } catch (e) {
          Logger.log(e);
        }
    }

    function mandaTweet(tweet) {
      // PON TUS PROPIAS CLAVES
      var TWITTER_CONSUMER_KEY = "TWITTER_CONSUMER_KEY";
      var TWITTER_CONSUMER_SECRET = "TWITTER_CONSUMER_SECRET";
      var TWITTER_ACCESS_TOKEN =  "TWITTER_ACCESS_TOKEN";
      var TWITTER_ACCESS_SECRET = "TWITTER_ACCESS_SECRET";
      var tweet = tweet; 
      if (typeof OAuth1 === 'undefined') {
        var libUrl = 'https://developers.google.com/adwords/scripts/docs/examples/oauth10-library';
        throw Error('OAuth1 library not found. Please take a copy of the OAuth1 ' +
            'library from ' + libUrl + ' and append to the bottom of this script.');
      }
      var params = {};
      var options = {method: 'POST', payload: {status: tweet}};
      var authUrlFetch = OAuth1.withAccessToken(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET,
          TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET);
      var response = authUrlFetch
          .fetch('https://api.twitter.com/1.1/statuses/update.json', params,
          options);
    }
  3. Configura el robot.

    En el archivo Código.gs debes editar las partes que están en rojo poniendo tus propios datos:
    • Aquí debes indicar la URL del feed de tu blog.
    • Si quieres añadir una frase antes del título del artículo aquí puedes editarla, si lo dejas como está se twitteará "Nuevo artículo en mi blog!! Título de tu artículo + enlace a tu artículo". Si no quieres que se añada ninguna frase, elimina el contenido en rojo.
    • ¿Recuerdas que antes anotastes las claves de tu aplicación de Twitter? Aquí es donde vamos a utilizarlas. Cada una en su lugar correspondiente.
  4. Autoriza tu cuenta de Google para utilizar un servicio externo.

    ¡Vamos a lanzar por primera vez tu automatizador! Tenemos que pulsar sobre "Seleccionar función" y elegir LanzaBot:

    A continuación, lanzamos por primera vez el bot:


    Si todo ha ido bien Google Script te avisará de que el programa necesita permiso para acceder a nuestros datos en Google. Esto ocurre porque necesitamos autorizar la conexión con el servicio externo como vemos a continuación. Evidentemente aceptamos todo.


    Ahora ya puedes ir a comprobar si se ha twitteado el último post de tu blog, de esa manera verificaremos que todo funciona correctamente. También lo puedes hacer desde Google Apps Script pulsando sobre "Ver" y luego sobre "Registros", el mensaje que debe aparecer es el siguiente (pero con tu título y tu URL):

    Podemos volver a verificar que todo va a ir bien. Si volvemos a ejecutar el script, es decir, volvemos alanzar el bot, podemos ver que identifica que es el mismo post y no lo twittea, pero nos avisa en el registro:


  5. Programa cuándo se ejecutará el programa.

    En este punto tan sólo tenemos que decirle al sistema cada cuánto tiempo queremos que "pase" por nuestro blog a ver si hay nuevos artículos que twittear. Esto dependerá de la frecuencia de publicación que tengamos, ya que como vemos a continuación, Google Apps Script te ofrece diversas opciones. Pulsamos sobre "Activadores del proyecto activo"...


    ...y configuramos uno nuevo. Podemos ver las distintas opciones que nos ofrece la plataforma, así que nos adaptaremos a nuestro ritmo de publicación.



    Si lo deseas, también puedes añadir un notificador para que te avise en caso de error de la secuencia, también con varias opciones:

Y esto es todo amigos y amigas, un sistema que, aunque yo me líe un poco para explicarlo, es bastante sencillo de aplicar y utilizar como alternativa a cualquier plataforma de terceros.




Llevo bastante tiempo "haciendo cositas" con Google Apps Script y esta es la primera vez que comparto una de ellas, espero que les sea muy útil!

Suscribirte por RSS: Rss