You are here

twitter.inc in Twitter 6.2

A wrapper API for the Twitter microblogging service.

Provides functions for retrieving public and authenticated messages from Twitter, helper code for locally caching Twitter data and account information, and utility functions for associating Drupal site users with their Twitter accounts.

File

twitter.inc
View source
<?php

/**
 * @file
 * A wrapper API for the Twitter microblogging service.
 *
 * Provides functions for retrieving public and authenticated messages from
 * Twitter, helper code for locally caching Twitter data and account
 * information, and utility functions for associating Drupal site users with
 * their Twitter accounts.
 */

/**
 * Generate a twitter posting form for the given user.
 *
 * @param $account
 *   A Drupal user object.
 */
function twitter_form($account = NULL) {
  drupal_add_js(drupal_get_path('module', 'twitter') . '/twitter.js', 'module');
  if (empty($account)) {
    global $user;
    $account = $user;
  }
  $twitter_accounts = drupal_map_assoc(array_keys(twitter_get_user_accounts($account->uid, TRUE)));
  if (count($twitter_accounts)) {
    $form = array();
    $form['status'] = array(
      '#type' => 'textfield',
      '#id' => 'twitter-textfield',
    );
    if (count($twitter_accounts) > 1) {
      $form['account'] = array(
        '#type' => 'select',
        '#title' => t('Account'),
        '#options' => $twitter_accounts,
        '#id' => 'twitter-account',
      );
    }
    else {
      $form['account'] = array(
        '#type' => 'value',
        '#value' => array_pop(array_keys($twitter_accounts)),
      );
    }
    return $form;
  }
}

/**
 * Twitter API functions
 */

/**
 * Fetch the public timeline for a Twitter.com account.
 *
 * Note that this function only requires a screen name, and not a password. As
 * such, it can only retrieve statuses for publically visible Twitter accounts.
 * Because it doesn't require authentication, it is also easier on the Twitter
 * servers. Be kind, use this version whenever you can.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $filter_since
 *   A boolean indicating that Twitter should only return statuses that have not
 *   been locally cached. This incurs an extra database hit, to retrieve the date
 *   of the most recent locally cached twitter message for the screen name.
 * @param $cache
 *   A boolean indicating whether the statuses should be cached in the local
 *   site's database after they're retrieved.
 * @return
 *   An array of Twitter statuses.
 *
 * @see twitter_fetch_statuses()
 */
function twitter_fetch_timeline($screen_name, $filter_since = TRUE, $cache = TRUE) {
  if ($filter_since) {
    $sql = "SELECT t.created_at FROM {twitter} t WHERE t.screen_name = '%s' ORDER BY t.created_at DESC";
    $since = db_result(db_query($sql, $screen_name));
  }
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/statuses/user_timeline/{$screen_name}.xml";
  if (!empty($since)) {
    $url .= '?since=' . urlencode($since);
  }
  $results = drupal_http_request($url, array(), 'GET');
  if (_twitter_request_failure($results)) {
    return array();
  }
  else {
    $results = _twitter_convert_xml_to_array($results->data);
    if ($cache) {
      foreach ($results as $status) {
        twitter_cache_status($status);
      }
      twitter_touch_account($screen_name);
    }
    return $results;
  }
}

/**
 * Post a message to a Twitter.com account.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @param $text
 *   The text to post. Strings longer than 140 characters will be truncated by
 *   Twitter.
 * @param $source
 *   A string indicating the program or site used to post the message. Source
 *   strings should be registered with Twitter, as unrecgonized sources are
 *   ignored.
 * @return
 *   The full results of the Drupal HTTP request, including the HTTP response
 *   code returned by Twitter.com.
 */
function twitter_set_status($screen_name, $password, $text, $source = 'drupal') {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/statuses/update.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $data = 'status=' . urlencode($text);
  if (!empty($source) && variable_get('twitter_set_source', TRUE)) {
    $data .= "&source=" . urlencode($source);
  }
  return drupal_http_request($url, $headers, 'POST', $data);
}

/**
 * Send a direct message to another Twitter user.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @param $to
 *   The ID or screen name of the recipient user. 
 * @param $text
 *   The text to post. Strings longer than 140 characters will be truncated by
 *   Twitter.
 * @return
 *   The full results of the Drupal HTTP request, including the HTTP response
 *   code returned by Twitter.com.
 */
function twitter_send_dm($screen_name, $password, $to, $text) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/direct_messages/new.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $data = 'text=' . urlencode($text);
  $data .= '&user=' . $to;
  return drupal_http_request($url, $headers, 'POST', $data);
}

/**
 * Fetch the full information for a Twitter.com account.
 *
 * This function requires an authenticated connection for the account in
 * question.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @param $cache
 *   A boolean indicating whether the account info should be cached in the local
 *   site's database after it's retrieved.
 * @return
 *   An single Twitter account.
 */
function twitter_fetch_account_info($screen_name, $password, $cache = TRUE) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/users/show/{$screen_name}.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $results = drupal_http_request($url, $headers, 'GET');
  if (_twitter_request_failure($results)) {
    return array();
  }
  if ($results = _twitter_convert_xml_to_array($results->data)) {
    if ($cache) {
      foreach ($results as $user) {
        twitter_cache_account($user);
      }
    }
    return $results[0];
  }
  return array();
}

/**
 * Fetch the latest statuses for a Twitter.com account, regardless of privacy.
 *
 * This function is the authenticated version of twitter_fetch_timeline(), and
 * is the only way to retrieve statuses for a 'private' account.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @param $cache
 *   A boolean indicating whether the statuses should be cached in the local
 *   site's database after they're retrieved.
 * @return
 *   An array of Twitter statuses.
 *
 * @see twitter_fetch_timeline()
 */
function twitter_fetch_statuses($screen_name, $password, $cache = TRUE) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/statuses/user_timeline.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $results = drupal_http_request($url, $headers, 'GET');
  if (_twitter_request_failure($results)) {
    return array();
  }
  $results = _twitter_convert_xml_to_array($results->data);
  if ($cache && !empty($results)) {
    foreach ($results as $status) {
      twitter_cache_status($status);
    }
    twitter_touch_account($screen_name);
  }
  return $results;
}

/**
 * Fetch information about Twitter.com accounts followed by a given user.
 *
 * This function does not require authentication. It is mostly useful for mining
 * information about connections, and locating existing Twitter friends who have
 * signed up for the same Drupal site.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @return
 *   An array of Twitter accounts.
 *
 * @see twitter_fetch_followers()
 */
function twitter_fetch_friends($screen_name) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/statuses/friends/{$screen_name}.xml";
  $results = drupal_http_request($url, array(), 'GET');
  if (_twitter_request_failure($results)) {
    return array();
  }
  return _twitter_convert_xml_to_array($results->data);
}

/**
 * Fetch information about users following a given Twitter.com account.
 *
 * This function is mostly useful for mining information about connections, and
 * locating existing Twitter friends who have also signed up for the same Drupal
 * site.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @return
 *   An array of Twitter accounts.
 *
 * @see twitter_fetch_friends()
 */
function twitter_fetch_followers($screen_name, $password) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/statuses/followers/{$screen_name}.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $results = drupal_http_request($url, $headers, 'GET');
  if (_twitter_request_failure($results)) {
    return array();
  }
  return _twitter_convert_xml_to_array($results->data);
}

/**
 * Attempts to authenticate a username/password on Twitter.com.
 *
 * @param $screen_name
 *   The screen name of a Twitter.com user.
 * @param $password
 *   The password of a Twitter.com user.
 * @return
 *   A boolean indicating success or failure.
 */
function twitter_authenticate($screen_name, $password) {
  $url = "http://" . variable_get('twitter_api_url', 'twitter.com') . "/account/verify_credentials.xml";
  $headers = array(
    'Authorization' => 'Basic ' . base64_encode($screen_name . ':' . $password),
    'Content-type' => 'application/x-www-form-urlencoded',
  );
  $results = drupal_http_request($url, $headers, 'GET');
  drupal_http_request('http://' . variable_get('twitter_api_url', 'twitter.com') . '/account/end_session', $headers, 'GET');
  return $results->code == '200';
}

/**
 * Internal helper function to deal cleanly with various HTTP response codes.
 */
function _twitter_request_failure($results) {
  switch ($results->code) {
    case '304':

      // 304 Not Modified: there was no new data to return.
      return TRUE;
    case 400:

      // 400 Bad Request: your request is invalid, and we'll return an error message that tells you why. This is the status code returned if you've exceeded the rate limit
      watchdog('twitter', '400 Bad Request.');
      return TRUE;
    case 401:

      // 401 Not Authorized: either you need to provide authentication credentials, or the credentials provided aren't valid.
      watchdog('twitter', '401 Not Authorized.');
      return TRUE;
    case 403:

      // 403 Forbidden: we understand your request, but are refusing to fulfill it.  An accompanying error message should explain why.
      watchdog('twitter', '403 Forbidden.');
      return TRUE;
    case 404:

      // 404 Not Found: either you're requesting an invalid URI or the resource in question doesn't exist (ex: no such user).
      watchdog('twitter', '404 Not Found.');
      return TRUE;
    case 500:

      // 500 Internal Server Error: we did something wrong.  Please post to the group about it and the Twitter team will investigate.
      watchdog('twitter', '500 Internal Server Error.');
      return TRUE;
    case 502:

      // 502 Bad Gateway: returned if Twitter is down or being upgraded.
      watchdog('twitter', '502 Bad Gateway.');
      return TRUE;
    case 503:

      // 503 Service Unavailable: the Twitter servers are up, but are overloaded with requests.  Try again later.
      watchdog('twitter', '503 Service Unavailable.');
      return TRUE;
  }

  // 200 OK: everything went awesome.
  return FALSE;
}

/**
 * Caching functions
 */

/**
 * Saves Twitter account information to the database.
 *
 * @param $twitter_account
 *   A Twitter user account in array form.
 *
 * @see twitter_touch_account()
 * @see twitter_cache_status()
 */
function twitter_cache_account($twitter_account = array()) {

  // convert boolean into int since it doesn't appear to be
  // handled correctly otherwise
  if ($twitter_account['protected']) {
    $twitter_account['protected'] = 1;
  }
  else {
    $twitter_account['protected'] = 0;
  }
  db_query("DELETE FROM {twitter_account} WHERE twitter_uid = %n", $twitter_account['twitter_uid']);
  drupal_write_record('twitter_account', $twitter_account);
}

/**
 * Updates the 'last refreshed on' timestamp of a given locally cached Twitter
 * account.
 *
 * @param $screen_name
 *   A Twitter screen name..
 *
 * @see twitter_cache_account()
 * @see twitter_cache_status()
 */
function twitter_touch_account($screen_name = '') {
  db_query("UPDATE {twitter_account} SET last_refresh = %d WHERE screen_name = '%s'", time(), $screen_name);
}

/**
 * Saves Twitter status message to the database.
 *
 * If the $silent parameter is set to TRUE, this function will also notify other
 * modules via hook_twitter_status_update() that a new stauts has been retrieved
 * and saved. This is normally set to FALSE, but may be useful when integrating
 * Twitter into complex workflows.
 *
 * @param $status
 *   A Twitter status updated in array form.
 * @param $silent
 *   A boolean indicating whether hook_twitter_status_update should be fired.
 *
 * @see twitter_touch_account()
 * @see twitter_cache_status()
 */
function twitter_cache_status($status = array(), $silent = FALSE) {
  db_query("DELETE FROM {twitter} WHERE twitter_id = %n", $status['twitter_id']);
  drupal_write_record('twitter', $status);
  if (!$silent) {
    module_invoke_all('twitter_status_update', $status);
  }
}

/**
 * User/account relationship code
 */
function twitter_get_user_accounts($uid, $full_access = FALSE) {
  $drupal_user = user_load($uid);
  return module_invoke_all('twitter_accounts', $drupal_user, $full_access);
}
function twitter_user_save($account = array(), $force_import = FALSE) {
  $account += array(
    'screen_name' => '',
    'import' => 1,
  );
  if (db_result(db_query("SELECT 1 FROM {twitter_user} WHERE uid = %d AND screen_name = '%s'", $account['uid'], $account['screen_name']))) {
    drupal_write_record('twitter_user', $account, array(
      'uid',
      'screen_name',
    ));
  }
  else {
    drupal_write_record('twitter_user', $account);
  }
  if ($force_import && $account['import']) {
    if (empty($account['protected']) || empty($account['password'])) {
      $statuses = twitter_fetch_timeline($account['screen_name']);
    }
    else {
      twitter_fetch_account_info($account['screen_name'], $account['password']);
      $statuses = twitter_fetch_statuses($account['screen_name'], $account['password']);
    }
    if (!empty($statuses)) {
      twitter_cache_account($statuses[0]['account']);
      twitter_touch_account($account['screen_name']);
    }
  }
}
function twitter_user_delete($uid, $screen_name = NULL) {
  $sql = "DELETE FROM {twitter_user} WHERE uid = %d";
  $args = array(
    $uid,
  );
  if (!empty($screen_name)) {
    $sql .= " AND screen_name = '%s'";
    $args[] = $screen_name;
  }
  db_query($sql, $args);
}

/**
 * Internal XML munging code
 */
function _twitter_convert_xml_to_array($data) {
  $results = array();
  try {
    $xml = new SimpleXMLElement($data);
  } catch (Exception $e) {
    watchdog('twitter', t('Convering XML to array failed. One possible reason is that ' . 'Twitter is down now. The convert failed with the following error: !exception.'), array(
      '!exception' => $e,
    ));
    return $results;
  }
  if (!empty($xml->name)) {

    // Top-level user information.
    $results[] = _twitter_convert_user($xml);
    return $results;
  }
  if (!empty($xml->user)) {
    foreach ($xml->user as $user) {
      $results[] = _twitter_convert_user($user);
    }
  }
  elseif (!empty($xml->status)) {
    foreach ($xml->status as $status) {
      $results[] = _twitter_convert_status($status);
    }
  }
  return $results;
}
function _twitter_convert_status($status) {
  $result = (array) $status;
  $result['twitter_id'] = $result['id'];
  if (!empty($result['user']) && is_object($result['user'])) {
    $result['account'] = _twitter_convert_user($result['user']);
    $result['screen_name'] = $result['account']['screen_name'];
  }
  else {
    $result['screen_name'] = NULL;
  }
  $result['created_time'] = strtotime($result['created_at']);

  // These come in as objects rather than strings IF they are empty, curiously
  // enough. We want nulls, so we'll special case them.
  foreach (array(
    'in_reply_to_status_id',
    'in_reply_to_user_id',
    'in_reply_to_screen_name',
  ) as $key) {
    if (is_object($result[$key])) {
      $result[$key] = NULL;
    }
  }
  return $result;
}
function _twitter_convert_user($user) {
  $result = (array) $user;
  $result['twitter_uid'] = $result['id'];
  if (!empty($result['status']) && is_object($result['status'])) {
    $result['status'] = _twitter_convert_status($result['status']);
  }
  return $result;
}
function _twitter_account_fields($user, $account = array()) {
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $user->uid,
  );
  $form['screen_name'] = array(
    '#type' => 'textfield',
    '#required' => TRUE,
    '#title' => t('Twitter user name'),
    '#default_value' => $twitter['screen_name'],
  );
  $form['password'] = array(
    '#type' => 'password',
    '#required' => TRUE,
    '#title' => t('Password'),
    '#default_value' => $twitter['password'],
  );
  return $form;
}

/**
 * This function exists because we have an extra-special shorturl token
 * whose value should only be generated if the token is used. This problem
 * goes away in D7's on-demand token system.
 */
function _twitter_replace_tokens($text, $node) {
  if (module_exists('token')) {
    $full = token_get_values('node', $node);
    if (strstr($text, '[shorturl]') !== FALSE) {
      $full->tokens[] = '[shorturl]';
      $full->values[] = twitter_shorten_url(url('node/' . $node->nid, array(
        'absolute' => TRUE,
      )));
    }
    return _token_replace_tokens($text, $full->tokens, $full->values);
  }
}

Functions

Namesort descending Description
twitter_authenticate Attempts to authenticate a username/password on Twitter.com.
twitter_cache_account Saves Twitter account information to the database.
twitter_cache_status Saves Twitter status message to the database.
twitter_fetch_account_info Fetch the full information for a Twitter.com account.
twitter_fetch_followers Fetch information about users following a given Twitter.com account.
twitter_fetch_friends Fetch information about Twitter.com accounts followed by a given user.
twitter_fetch_statuses Fetch the latest statuses for a Twitter.com account, regardless of privacy.
twitter_fetch_timeline Fetch the public timeline for a Twitter.com account.
twitter_form Generate a twitter posting form for the given user.
twitter_get_user_accounts User/account relationship code
twitter_send_dm Send a direct message to another Twitter user.
twitter_set_status Post a message to a Twitter.com account.
twitter_touch_account Updates the 'last refreshed on' timestamp of a given locally cached Twitter account.
twitter_user_delete
twitter_user_save
_twitter_account_fields
_twitter_convert_status
_twitter_convert_user
_twitter_convert_xml_to_array Internal XML munging code
_twitter_replace_tokens This function exists because we have an extra-special shorturl token whose value should only be generated if the token is used. This problem goes away in D7's on-demand token system.
_twitter_request_failure Internal helper function to deal cleanly with various HTTP response codes.