? .DS_Store
? twitter_views_cron_docs_admin_ponies.patch
Index: twitter.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.inc,v
retrieving revision 1.2
diff -u -p -r1.2 twitter.inc
--- twitter.inc	8 Jun 2008 04:56:45 -0000	1.2
+++ twitter.inc	16 Jun 2008 13:38:17 -0000
@@ -2,27 +2,38 @@
 // $Id: twitter.inc,v 1.2 2008/06/08 04:56:45 walkah Exp $
 
 /**
+ * @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 object $account user account object.
+ * @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)));
   if (count($twitter_accounts)) {
     $form = array();
     $form['status'] = array(
-      '#type' => 'textarea',
-      '#rows' => 1,
-      '#id' => 'twitter-textarea',
+      '#type' => 'textfield',
+      '#id' => 'twitter-textfield',
     );
-    
+
     if (count($twitter_accounts) > 1) {
       $form['account'] = array(
         '#type' => 'select',
@@ -42,23 +53,35 @@ function twitter_form($account = NULL) {
   }
 }
 
+
+
 /**
  * Twitter API functions
  */
-function twitter_set_status($screen_name, $password, $text = '', $source = NULL) {
-  $url = "http://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)) {
-    $data .= 'source='. urlencode($source);
-  }
-
-  return drupal_http_request($url, $headers, 'POST', $data);
-}
-
-function twitter_fetch_user_timeline($screen_name, $filter_since = TRUE, $cache = TRUE) {
+/**
+ * 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));
@@ -71,7 +94,7 @@ function twitter_fetch_user_timeline($sc
   }
 
   $results = drupal_http_request($url, array(), 'GET');
-  if ($results->code == 304) {
+  if (_twitter_request_failure($results)) {
     return array();
   }
   else {
@@ -80,45 +103,177 @@ function twitter_fetch_user_timeline($sc
       foreach($results as $status) {
         twitter_cache_status($status);
       }
+      twitter_touch_account($screen_name);
     }
     return $results;
   }
 }
 
-function twitter_fetch_user_friends($screen_name, $cache = TRUE) {
-  $url = "http://twitter.com/statuses/friends/$screen_name.xml";
-  $results = drupal_http_request($url, array(), 'GET');
-  return _twitter_convert_xml_to_array($results->data);
+
+/**
+ * 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. This is
+ *   not cleanly supported yet by Twitter, and should probably be 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 = NULL) {
+  $url = "http://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)) {
+    $data .= 'source='. urlencode($source);
+  }
+
+  return drupal_http_request($url, $headers, 'POST', $data);
 }
 
-function twitter_fetch_user_followers($screen_name, $password, $cache = TRUE) {
-  $url = "http://twitter.com/statuses/followers/$screen_name.xml";
+/**
+ * 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://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();
+  }
+
   $results = _twitter_convert_xml_to_array($results->data);
 
   if ($cache) {
-    foreach($results as $status) {
-      twitter_cache_status($status);
+    foreach($results as $user) {
+      twitter_cache_account($user);
     }
   }
   return $results;
 }
 
-function twitter_fetch_status($screen_name, $cache = TRUE) {
+/**
+ * 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://twitter.com/statuses/$screen_name.xml";
-  $results = drupal_http_request($url, array(), 'GET');
+  $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);
-      return $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://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://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://twitter.com/account/verify_credentials.xml";
   $headers = array('Authorization' => 'Basic '. base64_encode($screen_name .':'. $password),
@@ -128,33 +283,82 @@ function twitter_authenticate($screen_na
   return ($results->code == '200');
 }
 
-function twitter_fetch_user_details($screen_name, $password, $cache = TRUE) {
-  $url = "http://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 ($results->code == 401) {
-    return array();
-  }
-  $results = _twitter_convert_xml_to_array($results->data);
+/**
+ * Internal helper function to deal cleanly with various HTTP response codes.
+ */
+function _twitter_request_failure($results) {
+  switch ($results->code) {
+    case '304':
+      // Not modified, nothing to do.
+      return TRUE;
+    case 401:
+    case 403:
+      // Twitter returns both of these for different classes of auth failure.
+      watchdog('twitter', 'Twitter account could not be authenticated.');
+      return TRUE;
+    case 404:
+      // Probably a bogus username.
+      watchdog('twitter', 'Twitter account could not be retrieved.');
+      return TRUE;
 
-  if ($cache) {
-    foreach($results as $user) {
-      twitter_cache_user($user);
-    }
   }
-  return $results;
+  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()) {
+  db_query("DELETE FROM {twitter_account} WHERE twitter_uid = %d", $twitter_account['twitter_uid']);
+  drupal_write_record('twitter_account', $twitter_account);
 }
 
 
 /**
- * Helper functions
+ * 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_cache_user($twitter_user = array()) {
-  db_query("DELETE FROM {twitter_account} WHERE twitter_uid = %d", $twitter_user['twitter_uid']);
-  drupal_write_record('twitter_account', $twitter_user);
+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 = %d", $status['twitter_id']);
   drupal_write_record('twitter', $status);
@@ -164,8 +368,14 @@ function twitter_cache_status($status = 
 }
 
 
+
+
+/**
+ * User/account relationship code
+ */
+
 function twitter_get_user_accounts($uid) {
-  $sql = "SELECT *, tu.screen_name AS screen_name FROM {twitter_user} tu LEFT JOIN {twitter_account} ta ON (tu.screen_name = ta.screen_name) WHERE tu.uid = %d";
+  $sql = "SELECT ta.*, tu.uid, tu.password, tu.import FROM {twitter_user} tu LEFT JOIN {twitter_account} ta ON (tu.screen_name = ta.screen_name) WHERE tu.uid = %d";
   $args = array($uid);
   $results = db_query($sql, $args);
 
@@ -176,39 +386,41 @@ function twitter_get_user_accounts($uid)
   return $accounts;
 }
 
-function twitter_user_save($user = array()) {
+function twitter_user_save($user = array(), $force_import = FALSE) {
   $user += array(
-    'screen_name'       => '',
-    'password'          => '',
-    'last_refresh'      => 0,
-    'show_on_profile'   => 1,
-    'show_in_blocks'    => 1,
-    'announce_types'        => array(),
-    'announce_message'      => '',
+    'screen_name' => '',
+    'import'      => 1,
   );
-  
-  $user['announce_types'] = serialize($user['announce_types']);
+
   if (db_result(db_query("SELECT 1 FROM {twitter_user} WHERE uid = %d AND screen_name = '%s'", $user['uid'], $user['screen_name']))) {
     drupal_write_record('twitter_user', $user, array('uid', 'screen_name'));
   }
   else {
     drupal_write_record('twitter_user', $user);
   }
-  $user['announce_types'] = unserialize($user['announce_types']);
 
-  twitter_fetch_user_details($user['screen_name'], $user['password']);
+  if ($force_import && $account['import']) {
+    $account = twitter_fetch_account_info($user['screen_name'], $user['password']);
+    if (empty($account['protected'])) {
+      $statuses = twitter_fetch_timeline($account['screen_name']);
+    }
+    else {
+      $statuses = twitter_fetch_statuses($account['screen_name'], $account['password']);
+    }
+  }
 }
 
 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 tu.screen_name = '%s'";
+    $sql .= " AND screen_name = '%s'";
     $args[] = $screen_name;
   }
   db_query($sql, $args);
 }
 
+
 /**
  * Internal XML munging code
  */
@@ -254,9 +466,6 @@ function _twitter_convert_user($user) {
   if (!empty($result['status']) && is_object($result['status'])) {
     $result['status'] = _twitter_convert_status($result['status']);
   }
-  else {
-    $result['twitter_uid'] = NULL;
-  }
   return $result;
 }
 
Index: twitter.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.install,v
retrieving revision 1.1
diff -u -p -r1.1 twitter.install
--- twitter.install	8 Jun 2008 04:48:33 -0000	1.1
+++ twitter.install	16 Jun 2008 13:38:17 -0000
@@ -55,23 +55,34 @@ function twitter_schema() {
         'type' => 'int',
         'not null' => TRUE
       ),
-      'name' => array(
+      'screen_name' => array(
         'description' => t("The unique login name of the {twitter_account} user."),
         'type' => 'varchar',
+        'length' => 255
+      ),
+      'name' => array(
+        'description' => t("The full name of the {twitter_account} user."),
+        'type' => 'varchar',
         'length' => 64,
         'not null' => TRUE,
         'default' => ''
       ),
-      'screen_name' => array(
-        'description' => t("The full name of the {twitter_account} user."),
+      'description' => array(
+        'description' => t("The description/biography associated with the {twitter_account}."),
         'type' => 'varchar',
         'length' => 255
       ),
-      'description' => array(
-        'description' => t("The description/biography associated with the {twitter_account}."),
+      'location' => array(
+        'description' => t("The location of the {twitter_account}'s owner."),
         'type' => 'varchar',
         'length' => 255
       ),
+      'followers_count' => array(
+        'description' => t("The number of users following this {twitter_account}."),
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0
+      ),
       'profile_image_url' => array(
         'description' => t("The url of the {twitter_account}'s profile image."),
         'type' => 'varchar',
@@ -83,16 +94,17 @@ function twitter_schema() {
         'length' => 255
       ),
       'protected' => array(
-        'description' => t("Boolean flag indicating whether the {twitter_account}'s posts are publically accessible."),
+        'description' => t("Boolean flag indicating whether the {twitter_account}'s posts are publicly accessible."),
         'type' => 'int',
-        'not null' => FALSE,
+        'unsigned' => TRUE,
+        'not null' => TRUE,
         'default' => 0
       ),
       'last_refresh' => array(
         'description' => t("A UNIX timestamp marking the date Twitter statuses were last fetched on."),
         'type' => 'int',
         'not null' => TRUE
-      ),      
+      ),
     ),
     'indexes' => array('screen_name' => array('screen_name')),
     'primary key' => array('twitter_uid'),
@@ -114,9 +126,17 @@ function twitter_schema() {
         'description' => t("The password for the Twitter account."),
         'type' => 'varchar',
         'length' => 64
-      ),            
+      ),
+      'import' => array(
+        'description' => t("Boolean flag indicating whether the {twitter_user}'s posts should be pulled in by the site."),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 1
+      ),
     ),
     'primary key' => array('uid', 'screen_name'),
+    'indexes' => array('screen_name' => array('screen_name'), 'uid' => array('uid'), 'import' => array('import')),
   );
 
   return $schema;
@@ -134,10 +154,37 @@ function twitter_install() {
  * Previous versions of the Twitter module had no database schema.
  * We're safe just running the basic install for update_1.
  */
-function twitter_update_1() {
+function twitter_update_6000() {
   twitter_install();
 }
 
+function twitter_update_6001() {
+  $ret = array();
+  $attributes = array(
+    'description' => t("Boolean flag indicating whether the {twitter_user}'s posts should be pulled in by the site."),
+    'unsigned' => TRUE,
+    'default' => 1,
+    'not null' => TRUE,
+  );
+  db_add_column($ret, 'twitter_user', 'import', 'int', $attributes);
+
+  $attributes = array(
+    'description' => t("The location of the {twitter_account}'s owner."),
+    'length' => 255
+  );
+  db_add_column($ret, 'twitter_account', 'location', 'varchar(255)', $attributes);
+
+  $attributes = array(
+    'description' => t("The number of users following this {twitter_account}."),
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 0
+  );
+  db_add_column($ret, 'twitter_account', 'followers_count', 'int', $attributes);
+
+  return $ret;
+}
+
 function twitter_uninstall() {
   // Remove tables.
   drupal_uninstall_schema('twitter');
Index: twitter.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.js,v
retrieving revision 1.1
diff -u -p -r1.1 twitter.js
--- twitter.js	8 Jun 2008 04:48:33 -0000	1.1
+++ twitter.js	16 Jun 2008 13:38:17 -0000
@@ -5,7 +5,7 @@
  * description.
  */
 $(document).ready(function() {
-  $("#twitter-textarea").keyup(function() {
+  $("#twitter-textfield").keyup(function() {
     var charsLeft = (140 - $(this).val().length);
     var descDiv = $(this).parent().parent().parent().children(".description");
     $(descDiv).html("<strong>" + charsLeft + "</strong> characters remaining");
@@ -17,19 +17,19 @@ $(document).ready(function() {
       $("#twitter-post-button").removeAttr('disabled');
     }
   });
-  
+
   if (!$("#twitter-toggle").attr("checked")) {
-    $("#twitter-textarea-wrapper").hide();
-    $("#twitter-account-wrapper").hide();    
+    $("#twitter-textfield-wrapper").hide();
+    $("#twitter-account-wrapper").hide();
   }
-  
+
   $("#twitter-toggle").bind("click", function() {
     if ($("#twitter-toggle").attr("checked")) {
-      $("#twitter-textarea-wrapper").show();
+      $("#twitter-textfield-wrapper").show();
       $("#twitter-account-wrapper").show();
     }
     else {
-      $("#twitter-textarea-wrapper").hide();
+      $("#twitter-textfield-wrapper").hide();
       $("#twitter-account-wrapper").hide();
     }
   });
Index: twitter.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.module,v
retrieving revision 1.2
diff -u -p -r1.2 twitter.module
--- twitter.module	8 Jun 2008 04:48:33 -0000	1.2
+++ twitter.module	16 Jun 2008 13:38:18 -0000
@@ -25,7 +25,7 @@ function twitter_menu() {
     'file' => 'twitter.pages.inc',
     'type' => MENU_LOCAL_TASK,
   );
-  
+
   return $items;
 }
 
@@ -33,8 +33,14 @@ function twitter_menu() {
  * Implementation of hook_perm()
  */
 function twitter_perm() {
-  array(
-    'add twitter accounts'
+  return array('add twitter accounts');
+}
+
+function twitter_theme() {
+  return array(
+    'twitter_account_list_form' => array(
+      'arguments' => array('form' => NULL),
+    )
   );
 }
 
@@ -65,16 +71,16 @@ function twitter_form_alter(&$form, $for
     $form['twitter'] += $twitter_form;
     $form['twitter']['status']['#default_value'] = variable_get('twitter_default_format', 'New post: !title (!url)');
     $form['twitter']['status']['#description'] = t('The given text will be posted to twitter.com. You can use !url, !title and !user as replacement text.');
-  } 
+  }
 }
 
 function twitter_nodeapi(&$node, $op, $a1) {
   module_load_include('inc', 'twitter');
-  
+
   switch ($op) {
     case 'insert':
     case 'update':
-      if ($node->status && $node->twitter['post']) {
+      if (!empty($node->status) && !empty($node->twitter) && !empty($node->twitter['post'])) {
         $twitter_accounts = twitter_get_user_accounts($node->uid);
         $replacements = array('!title' => $node->title,
                               '!url' => url('node/'. $node->nid, array('absolute' => TRUE)),
@@ -86,4 +92,50 @@ function twitter_nodeapi(&$node, $op, $a
       }
       break;
   }
-}
\ No newline at end of file
+}
+
+/**
+ * Implementation of hook_cron()
+ *
+ * Imports new Twitter statuses for site users, and deletes expired tweets.
+ */
+function twitter_cron() {
+  if (!variable_get('twitter_import', TRUE)) {
+    return;
+  }
+
+  module_load_include('inc', 'twitter');
+
+  // Pull up a list of Twitter accounts that are flagged for updating,
+  // sorted by how long it's been since we last updated them. This ensures
+  // that the most out-of-date accounts get updated first.
+
+  $sql  = "SELECT tu.screen_name, tu.password, ta.protected FROM {twitter_user} tu ";
+  $sql .= "LEFT JOIN {twitter_account} ta ON tu.screen_name = ta.screen_name ";
+  $sql .= "WHERE tu.import = 1 ORDER BY ta.last_refresh ASC";
+
+  $results = db_query_range($sql, 0, 20);
+  while ($account = db_fetch_array($results)) {
+    // Use the 'cheaper' unauthenticated version if the account isn't protected.
+    if (empty($account['protected'])) {
+      $statuses = twitter_fetch_timeline($account['screen_name']);
+    }
+    else {
+      $statuses = twitter_fetch_statuses($account['screen_name'], $account['password']);
+    }
+
+    // If we got results back, update the account information with the latest
+    // follower count, location, user picture, etc. Also, touch the account's
+    // last_refreshed timestamp so it won't get thrashed before other, staler
+    // accounts.
+    if (!empty($statuses)) {
+      twitter_cache_account($statuses[0]['account']);
+    }
+    twitter_touch_account($account['screen_name']);
+  }
+
+  // Nuke old statuses.
+  if ($age = variable_get('twitter_expire', 0)) {
+    db_query('DELETE {twitter} WHERE created_time < %d', time() - $age);
+  }
+}
Index: twitter.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.pages.inc,v
retrieving revision 1.1
diff -u -p -r1.1 twitter.pages.inc
--- twitter.pages.inc	8 Jun 2008 04:48:33 -0000	1.1
+++ twitter.pages.inc	16 Jun 2008 13:38:18 -0000
@@ -6,52 +6,171 @@
  */
 function twitter_admin_form() {
   $form = array();
-  
-  $form['twitter_default_format'] = array(
+
+  $form['import'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Twitter import'),
+    '#description' => t('Import and display the Twitter statuses of site users who have entered their Twitter account information.'),
+  );
+
+  $form['import']['twitter_import'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Import Twitter statuses'),
+    '#default_value' => variable_get('twitter_import', TRUE),
+  );
+
+  $periods = array(0 => t('Never'));
+  $periods += drupal_map_assoc(array(604800, 2419200, 7257600, 31449600), 'format_interval');
+  $form['import']['twitter_expire'] = array(
+    '#type' => 'select',
+    '#title' => t('Delete old statuses'),
+    '#default_value' => variable_get('amazon_refresh_schedule', 0),
+    '#options' => $periods
+  );
+
+  $form['posting'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Twitter posting'),
+    '#description' => t('Users with proper permissions will be given the option to post announcements to their Twitter accounts when they create new content.'),
+  );
+
+  $form['posting']['twitter_types'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Node types'),
+    '#options' => node_get_types('names'),
+    '#default_value' => variable_get('twitter_types', array('story' => 'story', 'blog' => 'blog')),
+  );
+
+  $form['posting']['twitter_default_format'] = array(
     '#type' => 'textfield',
     '#title' => t('Default format string'),
     '#maxlength' => 140,
-    '#description' => t('The given text will be posted to twitter.com. You can use !url, !title and !user as replacement text.')
+    '#description' => t('The given text will be posted to twitter.com. You can use !url, !title and !user as replacement text.'),
+    '#default_value' => variable_get('twitter_default_format', 'New post: !title (!url)'),
   );
-  
-  $form['twitter_types'] = array(
-    
-  );
- 
-  return system_settings_form($form); 
+
+  return system_settings_form($form);
 }
 
 function twitter_user_settings($account) {
   module_load_include('inc', 'twitter');
-  
+
+  $output = '';
   $twitter_accounts = twitter_get_user_accounts($account->uid);
-  
-  $header = array('', t('Name'), t('Description'));
-  $rows = array();
-  
+  if (!empty($twitter_accounts)) {
+    $output .= drupal_get_form('twitter_account_list_form', $twitter_accounts);
+  }
+  $output .= drupal_get_form('twitter_add_account', $account);
+
+  return $output;
+}
+
+function twitter_account_list_form($form_state, $twitter_accounts = array()) {
+  $form['#tree'] = TRUE;
+  $form['accounts'] = array();
+
   foreach ($twitter_accounts as $twitter_account) {
+    $form['accounts'][] = _twitter_account_list_row($twitter_account);
+  }
+
+  if (!empty($twitter_accounts)) {
+    $form['buttons']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save changes'),
+    );
+  }
+
+  return $form;
+}
+
+function _twitter_account_list_row($account) {
+  $form['#account'] = $account;
+
+  $form['uid'] = array(
+    '#type' => 'value',
+    '#value' => $account['uid'],
+  );
+
+  $form['screen_name'] = array(
+    '#type' => 'value',
+    '#value' => $account['screen_name'],
+  );
+
+  $form['image'] = array(
+    '#type' => 'markup',
+    '#value' => theme('image', $account['profile_image_url'], '', '', array(), FALSE),
+  );
+
+  $form['visible_name'] = array(
+    '#type' => 'markup',
+    '#value' => l($account['screen_name'], 'http://www.twitter.com/' . $account['screen_name']),
+  );
+
+  $form['description'] = array(
+    '#type' => 'markup',
+    '#value' => filter_xss($account['description']),
+  );
+
+  $form['protected'] = array(
+    '#type' => 'markup',
+    '#value' => empty($account['protected']) ? t('No') : t('Yes'),
+  );
+
+  $form['import'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => $account['import'],
+  );
+
+  $form['delete'] = array(
+    '#type' => 'checkbox',
+  );
+  return $form;
+}
+
+function theme_twitter_account_list_form($form) {
+  $header = array('', t('Name'), t('Description'), t('Private'), t('Import'), t('Delete'));
+  $rows = array();
+
+  foreach (element_children($form['accounts']) as $key) {
+    $element = &$form['accounts'][$key];
     $rows[] = array(
-      '<img src="'.$twitter_account['profile_image_url'].'" />',
-      $twitter_account['screen_name'],
-      $twitter_account['description'],
+      drupal_render($element['image']),
+      drupal_render($element['uid']) . drupal_render($element['screen_name']) . drupal_render($element['visible_name']),
+      drupal_render($element['description']),
+      drupal_render($element['protected']),
+      drupal_render($element['import']),
+      drupal_render($element['delete']),
     );
   }
 
   $output = theme('table', $header, $rows);
-  $output .= drupal_get_form('twitter_add_account', $account);
+  $output .= drupal_render($form);
   return $output;
 }
 
+function twitter_account_list_form_submit($form, &$form_state) {
+  $accounts = $form_state['values']['accounts'];
+  foreach($accounts as $account) {
+    if (empty($account['delete'])) {
+      twitter_user_save($account);
+    }
+    else {
+      twitter_user_delete($account['uid'], $account['screen_name']);
+    }
+  }
+}
+
 function twitter_add_account($form_state, $account = NULL) {
   if (empty($account)) {
     global $user;
     $account = $user;
   }
-  
+
   $form['uid'] = array(
     '#type' => 'value',
     '#value' => $account->uid,
   );
+
   $form['screen_name'] = array(
     '#type' => 'textfield',
     '#required' => TRUE,
@@ -63,17 +182,24 @@ function twitter_add_account($form_state
     '#title' => t('Password'),
   );
 
+  $form['import'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Import statuses from this account'),
+    '#default_value' => TRUE,
+    '#access' => FALSE,
+  );
+
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Add account'),
   );
 
-  return $form;  
+  return $form;
 }
 
 function twitter_add_account_validate($form, &$form_state) {
   module_load_include('inc', 'twitter');
-  
+
   $verify = FALSE;
 
   $pass = $form_state['values']['password'];
@@ -93,8 +219,8 @@ function twitter_add_account_validate($f
 
 function twitter_add_account_submit($form, &$form_state) {
   module_load_include('inc', 'twitter');
-   
+
   if (!empty($form_state['values']['screen_name'])) {
-    twitter_user_save($form_state['values']);
+    twitter_user_save($form_state['values'], TRUE);
   }
 }
Index: twitter.views.inc
===================================================================
RCS file: twitter.views.inc
diff -N twitter.views.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter.views.inc	16 Jun 2008 13:38:18 -0000
@@ -0,0 +1,340 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Provide views data and handlers for twitter.module
+ */
+
+/**
+ * @defgroup views_twitter_module twitter.module handlers
+ *
+ * Includes the ability to create views of just the twitter table.
+ * @{
+ */
+
+/**
+ * Implementation of hook_views_data()
+ */
+function twitter_views_data() {
+  // Basic table information.
+
+  $data['twitter']['table']['group']  = t('Twitter');
+
+  // Advertise this table as a possible base table
+  $data['twitter']['table']['base'] = array(
+    'field' => 'twitter_id',
+    'title' => t('Twitter message'),
+    'help' => t('Stores Twitter status messages.'),
+    'weight' => 10,
+  );
+
+  // Twitter screen name
+  $data['twitter']['screen_name'] = array(
+    'title' => t('Login name'),
+    'help' => t('The login account of the Twitter user.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+  );
+
+  // Twitter message timestamp
+  $data['twitter']['created_time'] = array(
+    'title' => t('Created time'),
+    'help' => t('The time the Twitter message was posted.'),
+    'field' => array(
+      'handler' => 'views_handler_field_date',
+      'click sortable' => TRUE,
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_date',
+    ),
+  );
+
+  // Twitter text
+  $data['twitter']['text'] = array(
+    'title' => t('Message text'),
+    'help' => t('The text of the Twitter message.'),
+    'field' => array(
+      'handler' => 'views_handler_field_xss',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+  );
+
+  // Twitter source
+  $data['twitter']['source'] = array(
+    'title' => t('Source'),
+    'help' => t('The name of the application that posted the Twitter message.'),
+    'field' => array(
+      'handler' => 'views_handler_field_xss',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+
+
+
+  $data['twitter_account']['table']['group']  = t('Twitter');
+
+  $data['twitter_account']['table']['join'] = array(
+    'twitter' => array(
+      'left_field' => 'screen_name',
+      'field' => 'screen_name',
+    ),
+    'users' => array(
+      'field' => 'screen_name',
+      'left_table' => 'twitter_user',
+      'left_field' => 'screen_name',
+    ),
+  );
+
+  // Twitter screen name
+  $data['twitter_account']['screen_name'] = array(
+    'title' => t('Login name'),
+    'help' => t('The login account of the Twitter user.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+  );
+
+
+  // Twitter account full name
+  $data['twitter_account']['name'] = array(
+    'title' => t('Full name'),
+    'help' => t('The full name Twitter account user.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+    'argument' => array(
+      'handler' => 'views_handler_argument_string',
+    ),
+  );
+
+  // Twitter account description
+  $data['twitter_account']['description'] = array(
+    'title' => t('Description'),
+    'help' => t('The description of the Twitter account.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_field_xss',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Twitter account location
+  $data['twitter_account']['location'] = array(
+    'title' => t('Location'),
+    'help' => t('The location of the Twitter account.'),
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_field_xss',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Twitter account description
+  $data['twitter_account']['followers_count'] = array(
+    'title' => t('Location'),
+    'help' => t('The number of users following this Twitter account.'),
+    'field' => array(
+      'handler' => 'views_handler_field_numeric',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_numeric',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Twitter account profile image
+  $data['twitter_account']['profile_image_url'] = array(
+    'title' => t('Profile image'),
+    'help' => t('The image used by the Twitter account.'),
+    'field' => array(
+      'handler' => 'twitter_views_handler_profile_image',
+      'click sortable' => TRUE,
+    ),
+  );
+
+  // Twitter account url
+  $data['twitter_account']['url'] = array(
+    'title' => t('URL'),
+    'help' => t('The URL given by the Twitter account user.'),
+    'field' => array(
+      'handler' => 'views_handler_field_url',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_string',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Twitter account protected
+  $data['twitter_account']['protected'] = array(
+    'title' => t('Protected status'),
+    'help' => t('Whether posts from this Twitter account should be visible to the general public.'),
+    'field' => array(
+      'handler' => 'views_handler_field_boolean',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_boolean_operator',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Twitter message timestamp
+  $data['twitter_account']['last_refresh'] = array(
+    'title' => t('Last refresh'),
+    'help' => t('The time the Twitter account statuses were retrieved.'),
+    'field' => array(
+      'handler' => 'views_handler_field_date',
+      'click sortable' => TRUE,
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_date',
+    ),
+  );
+
+
+  $data['twitter_user']['table']['group']  = t('Twitter');
+
+  $data['twitter_user']['table']['join'] = array(
+    'users' => array(
+      'left_field' => 'uid',
+      'field' => 'uid',
+    ),
+    'twitter_account' => array(
+      'left_field' => 'screen_name',
+      'field' => 'screen_name',
+    ),
+    'twitter' => array(
+      'left_table' => 'twitter_account',
+      'left_field' => 'screen_name',
+      'field' => 'screen_name',
+    ),
+  );
+
+  // Twitter account description
+  $data['twitter_user']['uid'] = array(
+    'title' => t('User ID'),
+    'help' => t('The UID of the Twitter account.'),
+  );
+
+  // Twitter account description
+  $data['twitter_user']['screen_name'] = array(
+    'title' => t('Screen name'),
+    'help' => t('The screen name of the Twitter account.'),
+  );
+
+  // Twitter account protected
+  $data['twitter_user']['import'] = array(
+    'title' => t('Import status'),
+    'help' => t('Whether posts from this Twitter account should be imported automatically.'),
+    'field' => array(
+      'handler' => 'views_handler_field_boolean',
+      'click sortable' => TRUE,
+    ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_boolean_operator',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+
+  return $data;
+}
+
+
+function twitter_views_data_alter(&$data) {
+  $data['users']['table']['join']['twitter'] = array(
+    'left_table' => 'twitter_user',
+    'left_field' => 'uid',
+    'field' => 'uid',
+  );
+  $data['users']['table']['join']['twitter_account'] = array(
+    'left_table' => 'twitter_user',
+    'left_field' => 'uid',
+    'field' => 'uid',
+  );
+  $data['users']['table']['join']['twitter_user'] = array(
+    'left_field' => 'uid',
+    'field' => 'uid',
+  );
+}
+
+
+/**
+ * Field handler to provide simple renderer that turns a URL into a clickable link.
+ */
+class twitter_views_handler_profile_image extends views_handler_field {
+  function render($values) {
+    $value = $values->{$this->field_alias};
+    return theme('image', $value, '', '', array(), FALSE);
+  }
+}
+
+
+/**
+ * @}
+ */
Index: twitter.views_default.inc
===================================================================
RCS file: twitter.views_default.inc
diff -N twitter.views_default.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter.views_default.inc	16 Jun 2008 13:38:18 -0000
@@ -0,0 +1,181 @@
+<?php
+// $Id$
+
+function twitter_views_default_views() {
+  $view = new view;
+  $view->name = 'tweets';
+  $view->description = 'Displays Twitter.com status messages for users who have associated Twitter accounts.';
+  $view->tag = '';
+  $view->view_php = '';
+  $view->base_table = 'twitter';
+  $view->is_cacheable = '0';
+  $view->api_version = 2;
+  $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+  $handler = $view->new_display('default', 'Tweets', 'default');
+  $handler->override_option('fields', array(
+    'profile_image_url' => array(
+      'label' => '',
+      'exclude' => 0,
+      'id' => 'profile_image_url',
+      'table' => 'twitter_account',
+      'field' => 'profile_image_url',
+      'relationship' => 'none',
+    ),
+    'text' => array(
+      'label' => '',
+      'exclude' => 0,
+      'id' => 'text',
+      'table' => 'twitter',
+      'field' => 'text',
+      'relationship' => 'none',
+    ),
+    'created_time' => array(
+      'label' => '',
+      'date_format' => 'time ago',
+      'custom_date_format' => '',
+      'exclude' => 0,
+      'id' => 'created_time',
+      'table' => 'twitter',
+      'field' => 'created_time',
+      'relationship' => 'none',
+    ),
+  ));
+  $handler->override_option('sorts', array(
+    'created_time' => array(
+      'order' => 'DESC',
+      'id' => 'created_time',
+      'table' => 'twitter',
+      'field' => 'created_time',
+      'relationship' => 'none',
+    ),
+  ));
+  $handler->override_option('arguments', array(
+    'uid' => array(
+      'default_action' => 'default',
+      'style_plugin' => 'default_summary',
+      'style_options' => array(),
+      'wildcard' => 'all',
+      'wildcard_substitution' => 'All author',
+      'title' => '%1\'s tweets',
+      'default_argument_type' => 'user',
+      'default_argument' => '',
+      'validate_type' => 'none',
+      'validate_fail' => 'not found',
+      'break_phrase' => 0,
+      'not' => 0,
+      'id' => 'uid',
+      'table' => 'users',
+      'field' => 'uid',
+      'relationship' => 'none',
+      'default_argument_user' => 1,
+      'default_argument_fixed' => '',
+      'default_argument_php' => '',
+      'validate_argument_node_type' => array(
+        'poll' => 0,
+        'page' => 0,
+        'review' => 0,
+        'story' => 0,
+      ),
+      'validate_argument_node_access' => 0,
+      'validate_argument_nid_type' => 'nid',
+      'validate_argument_vocabulary' => array(),
+      'validate_argument_type' => 'tid',
+      'validate_argument_php' => '',
+    ),
+  ));
+  $handler->override_option('filters', array(
+    'protected' => array(
+      'operator' => '=',
+      'value' => 0,
+      'group' => '0',
+      'exposed' => FALSE,
+      'expose' => array(
+        'operator' => FALSE,
+        'label' => '',
+      ),
+      'id' => 'protected',
+      'table' => 'twitter_account',
+      'field' => 'protected',
+      'relationship' => 'none',
+    ),
+  ));
+  $handler->override_option('access', array(
+    'type' => 'none',
+    'role' => array(),
+    'perm' => '',
+  ));
+  $handler->override_option('use_pager', 'mini');
+  $handler->override_option('style_plugin', 'table');
+  $handler->override_option('style_options', array(
+    'grouping' => '',
+    'override' => 1,
+    'sticky' => 0,
+    'order' => 'asc',
+    'columns' => array(
+      'profile_image_url' => 'profile_image_url',
+      'text' => 'text',
+      'created_time' => 'text',
+    ),
+    'info' => array(
+      'profile_image_url' => array(
+        'sortable' => 0,
+        'separator' => '',
+      ),
+      'text' => array(
+        'sortable' => 0,
+        'separator' => ' &#8212; ',
+      ),
+      'created_time' => array(
+        'sortable' => 0,
+        'separator' => '',
+      ),
+    ),
+    'default' => '-1',
+  ));
+  $handler = $view->new_display('page', 'Page', 'page');
+  $handler->override_option('path', 'user/%/tweets');
+  $handler->override_option('menu', array(
+    'type' => 'tab',
+    'title' => 'Twitter',
+    'weight' => '1',
+  ));
+  $handler->override_option('tab_options', array(
+    'type' => 'none',
+    'title' => '',
+    'weight' => 0,
+  ));
+  $handler = $view->new_display('block', 'Block', 'block');
+  $handler->override_option('fields', array(
+    'text' => array(
+      'label' => '',
+      'exclude' => 0,
+      'id' => 'text',
+      'table' => 'twitter',
+      'field' => 'text',
+      'relationship' => 'none',
+    ),
+    'created_time' => array(
+      'label' => '',
+      'date_format' => 'time ago',
+      'custom_date_format' => '',
+      'exclude' => 0,
+      'id' => 'created_time',
+      'table' => 'twitter',
+      'field' => 'created_time',
+      'relationship' => 'none',
+    ),
+  ));
+  $handler->override_option('items_per_page', 5);
+  $handler->override_option('use_more', 1);
+  $handler->override_option('style_plugin', 'list');
+  $handler->override_option('row_options', array(
+    'inline' => array(
+      'text' => 'text',
+      'created_time' => 'created_time',
+    ),
+    'separator' => ' &#8212; ',
+  ));
+  $handler->override_option('block_description', 'User Tweets');
+
+  return array('tweets' => $view);
+}
