Cómo publicar contenido de forma automática en Twitter

28/01/2015
twitter y drupal

Publicar un nodo y a la vez un tuit para decir a los seguidores de Twitter que se ha publicado un contenido nuevo es algo que posiblemente se le haya podido pasar por la cabeza a más de un cliente. A mí, como desarrollador, me parecía una idea muy atractiva desde hacía bastante tiempo, pero por unos motivos u otros hasta ahora no había tenido la oportunidad de implementarlo.

Introducción

A nivel técnico es necesario analizar si la solución se implementará mediante algún módulo contribuido o bien desarrollando una solución completamente adaptada al proyecto. En nuestro caso hemos optado por implementarlo nosotros puesto que los módulos que vimos abarcaban demasiada funcionalidad.

¿En qué ha consistido la solución?

  1. Configuración del módulo.
  2. Clase con la lógica de negocio que se usará en el diálogo con Twitter.
  3. Implementación de un hook para enviar un tweet cuando se guarda un nodo.

Configuración

Para publicar un tuit a través de la api de Twitter lo primero que debemos hacer dar permiso a la aplicación que estamos construyendo para poder escribir en el timeline del usuario. Para hacer esto es necesario hacerlo en http://apps.twitter.com . Si no sabes bien cómo, puedes leerte este artículo.

En nuestro módulo vamos a crear una entrada de menu que nos llevará a un formulario de configuración.

/**
 * Implements hook_menu().
 */
function twitter_autopublish_menu() {
  $items['admin/config/twitter_autopublish'] = array(
    'title' => t('Configuration for twitter_autopublish module'),
    'description' => t('Configuration for twitter_autopublish module.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('twitter_autopublish_settings'),
    'access callback' => 'user_access',
    'access arguments' => array('Administer twitter'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'twitter_autopublish.admin.inc',
  );
 
  return $items;
}

Esto nos llevará a un formulario bastante simple que almacenará en variables el valor de los parámetros: access_token, access_token_secret, consumer_key y consumer_key_secret

 
/**
 * @file
 * This file contains the functionality for managing the configuration for
 * twitter autopublish module.
 */
 
/**
 * This function is the callback for the configuration settings for the module
 */
function twitter_autopublish_settings() {
  $form = array();
  $form['twitter_autopublish_access_token'] = array(
    '#type' => 'textfield',
    '#title' => t('Access token'),
    '#default_value' => variable_get('twitter_autopublish_access_token', ''),
    '#size' => 75,
    '#maxlength' => 75,
    '#description' => t('Access token given for the application in twitter'),
    '#required' => TRUE,
  );
  $form['twitter_autopublish_access_token_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Access token secret'),
    '#default_value' => variable_get('twitter_autopublish_access_token_secret', ''),
    '#size' => 75,
    '#maxlength' => 75,
    '#description' => t('Access token secret given for the application in twitter'),
    '#required' => TRUE,
  );
  $form['twitter_autopublish_consumer_key'] = array(
    '#type' => 'textfield',
    '#title' => t('Consumer key'),
    '#default_value' => variable_get('twitter_autopublish_consumer_key', ''),
    '#size' => 75,
    '#maxlength' => 75,
    '#description' => t('Consumer key given for the application in twitter'),
    '#required' => TRUE,
  );
  $form['twitter_autopublish_consumer_key_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Consumer key secret'),
    '#default_value' => variable_get('twitter_autopublish_consumer_key_secret', ''),
    '#size' => 75,
    '#maxlength' => 75,
    '#description' => t('Consumer key secret given for the application in twitter'),
    '#required' => TRUE,
  );
  return system_settings_form($form);
}

Además, se definirá un permiso para poder administrar este formulario.

/**
 * Implements hook_permission().
 */
function twitter_autopublish_permission() {
  return array(
    'Administer twitter' => array(
        'title' => t('Administer twitter'),
        'description' => t('This permission allow people to configure the module twitter_autopublish for sending tweets'),
    ));
}

Lógica de negociación con Twitter

Basándonos en la documentación de la api de Twitter y simplificando la clase TwitterAPIExchange, construimos la clase TwitterTalker para realizar el diálogo. La clase original está pensada para realizar otras operaciones con la API de Twitter con lo que la simplificamos hasta dejarla como se muestra en el siguiente código:
<?php
 
/**
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 
/**
* This class is the manager for twitter conversations. Its responsablity is focused
* on manage the convesation with the Twitter API
* This class is based on TwitterAPIExchange class
* (https://github.com/J7mbo/twitter-api-php/blob/master/TwitterAPIExchange.php)
*/
class TwitterTalker {
  /** Signature method */
  const OAUTH_SIGNATURE_METHOD = 'HMAC-SHA1';
  /** Oauth version */
  const OAUTH_VERSION = '1.0';
  /** Include entities or not */
  const INCLUDE_ENTITIES = 'true';
 
  /** Consumer key of the twitter app */
  private $consumer_key;
  /** Private consmer secret string */
  private $consumer_secret;
  /** Access token */
  private $access_token;
  /** Secret token */
  private $access_token_secret;
 
  private $url;
  private $oauth;
  private $petition;
 
  /**
   * Constructor
   */
  function __construct($settings) {
    if (!isset($settings['consumer_key'])
      || !isset($settings['consumer_secret'])
      || !isset($settings['access_token'])
      || !isset($settings['access_token_secret'])) {
 
      throw new Exception("Some params are missing. Please, check that you configure all the params", 1);
    }
    $this->consumer_key = $settings['consumer_key'];
    $this->consumer_secret = $settings['consumer_secret'];
    $this->access_token = $settings['access_token'];
    $this->access_token_secret = $settings['access_token_secret'];
  }
 
  /**
   * @param $status Message to send to the timeline
   */
  public function createTweet($status) {
    $url = 'https://api.twitter.com/1.1/statuses/update.json';
    $this->getOAuth('post', $url);
    $params = array('status' => $status);
    $this->createPetition($params, $url);
  }
 
  /**
   * @param $method petition method
   * @param $url the url of the service
   */
  private function getOAuth($method, $url) {
    $oauth = array(
      'oauth_consumer_key' => $this->consumer_key,
      'oauth_nonce' => time(),
      'oauth_signature_method' =>  self::OAUTH_SIGNATURE_METHOD,
      'oauth_timestamp' => time(),
      'oauth_token' => $this->access_token,
      'oauth_version' => self::OAUTH_VERSION,
    );
 
    $base_url = $this->buildBaseURLString($method, $url, $oauth);
    $signature_key = rawurlencode($this->consumer_secret) . '&' . rawurlencode($this->access_token_secret);
    $signature = base64_encode(hash_hmac('SHA1', $base_url, $signature_key, true));
    $oauth['oauth_signature'] = $signature;
 
    $this->oauth = $oauth;
    $this->url = $url;
  }
 
  /* This method builds the base URL string for the OAuth
   */
  private function buildBaseURLString($method, $url, $oauth) {
    $return = array();
    ksort($oauth);
 
    foreach($oauth as $key=>$value)
    {
      $return[] = "$key=" . $value;
    }
    //The string start with the method in uppercase
    $url = strtoupper($method) . '&'
      . rawurlencode($url) . '&'
      . rawurlencode(implode('&', $return));
 
    return $url;
  }
 
  /**
   * This method
   * @param $params
   * @param $url
   */
  private function createPetition($params, $url) {
    $header[] = 'Authorization: OAuth '.
      'oauth_consumer_key="' . rawurlencode($this->oauth['oauth_consumer_key']) . '", ' .
      'oauth_nonce="' . rawurlencode($this->oauth['oauth_nonce']) . '", ' .
      'oauth_signature="' . rawurlencode($this->oauth['oauth_signature']) . '", ' .
      'oauth_signature_method="' . rawurlencode($this->oauth['oauth_signature_method']) . '", ' .
      'oauth_token="' . rawurlencode($this->oauth['oauth_token']) . '", ' .
      'oauth_timestamp="' . rawurlencode($this->oauth['oauth_timestamp']) . '", ' .
      'oauth_version="' . rawurlencode($this->oauth['oauth_version']) . '"';
 
    $options = array(
      CURLOPT_URL => $url,
      CURLOPT_HEADER => false,
      CURLOPT_HTTPHEADER => $header,
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_TIMEOUT => 10,
      CURLOPT_POSTFIELDS => $params,
    );
 
    $petition = curl_init();
    curl_setopt_array($petition, $options);
    $response = curl_exec($petition);
    curl_close($petition);
  }
}
?>
En esencia, se establecen lo valores de configuración que tenemos, se establece la URL de la llamada al servicio, se crea el objeto oAuth, se establece el mensaje a publicar y se llama al servicio. Casi todo esto ocurre en el método createTweet()

Implementación del hook

En este caso, lo que hacemos es sencillo. Implementamos el hook_node_insert como se muestra en el siguiente código:

/**
 * Implements hook_node_insert().
 */
function twitter_autopublish_node_insert($node) {
  $settings = array(
    'consumer_key' => variable_get('twitter_autopublish_consumer_key'),
    'access_token' => variable_get('twitter_autopublish_access_token'),
    'consumer_secret' => variable_get('twitter_autopublish_consumer_key_secret'),
    'access_token_secret' => variable_get('twitter_autopublish_access_token_secret'),
  );
  $twitter_talker = new TwitterTalker($settings);
  $status = 'Creado el nodo ' . $node->title . ' in #myHashtag';
  $twitter_talker->createTweet($status);
}

Aquí se crea un array a partir de la configuración establecida en el formulario del punto 1, se crea un objeto TwitterTalker y se define un mensaje y por último se llama al método createTweet().

Conclusiones

Por un lado, algo bastante positivo del módulo es que con muy poco se consigue bastante. Como hemos podido ver con solo tres hooks y una clase hemos implementado la solución a nuestro problema original.

Por otra parte, y mirando al futuro, implementar soluciones en este sentido te va a permitir migrar con facilidad cualquier módulo de la versión 7 de Drupal a la 8 ya que la lógica de peso del módulo está implementada en una clase que podrás trasladar sin más a la futura (y esperada) versión de Drupal.

Por último, os dejo el enlace al repositorio del módulo que os he presentado en esta entrada de blog.
https://github.com/Emergya/twitter_autopublish