? .DS_Store
? twitter_6.patch
Index: twitter.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.info,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 twitter.info
--- twitter.info	18 Jun 2007 23:07:10 -0000	1.1.2.1
+++ twitter.info	29 Jul 2007 23:03:35 -0000
@@ -1,5 +1,6 @@
-; $Id: twitter.info,v 1.1.2.1 2007/06/18 23:07:10 dww Exp $
+; $Id$
 name = Twitter
-description = This module posts blog updates to twitter.com
-
-
+description = Exposes the twitter.com APIs to other Drupal modules
+package = Twitter
+php = 5.1
+core = 6.x
Index: twitter.install
===================================================================
RCS file: twitter.install
diff -N twitter.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter.install	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,23 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function twitter_install() {
+  // Create tables.
+  drupal_install_schema('twitter');
+}
+
+/**
+ * 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() {
+  twitter_install();
+}
+
+function twitter_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('twitter');
+}
Index: twitter.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/twitter/twitter.module,v
retrieving revision 1.1
diff -u -p -r1.1 twitter.module
--- twitter.module	23 Jan 2007 17:05:50 -0000	1.1
+++ twitter.module	29 Jul 2007 23:03:35 -0000
@@ -1,67 +1,194 @@
 <?php
 // $Id: twitter.module,v 1.1 2007/01/23 17:05:50 walkah Exp $
 
-define('TWITTER_URL', 'http://twitter.com/statuses/update.xml');
-
 /**
- * Implementation of hook_user()
+ * @file
+ * Manages Twitter API integration for Drupal. Twitter data is stored
+ * (internal to this module, at least) as structured arrays in the following
+ * formats:
+ *
+ * $twitter_status = array(
+ *   'twitter_id'   => $unique_id_from_twitter,
+ *   'twitter_uid'  => $unique_user_id_from_twitter,
+ *   'twitter_user' => $optional_sub_array
+ *   'created_at'   => $time_of_post_in_UTC,
+ *   'created_time' => $unix_timestamp_of_post,
+ *   'text'         => $text_of_tweet,
+ *   'source'       => $source_application,
+ * );
+ *
+ * $twitter_account = array(
+ *   'twitter_uid'       => $unique_user_id_from_twitter,
+ *   'name'              => $twitter_login_email,
+ *   'screen_name'       => $twitter_screen_name,
+ *   'description'       => $user_description,
+ *   'profile_image_url' => $image_url,
+ *   'url'               => $user_home_page,
+ *   'protected'         => $boolean,
+ *   'status'            => $optional_sub_array
+ * );
  */
-function twitter_user($op, &$edit, &$account, $category = NULL) {
-  switch ($op) {
-    case 'form':
-      $form['twitter'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Twitter settings'));
-      $form['twitter']['twitter_user'] = array(
-        '#type' => 'textfield',
-        '#title' => t('Username'),
-        '#default_value' => $edit['twitter_user'],
-        '#description' => t('The username (email address) associated with your twitter.com account'));
-      $form['twitter']['twitter_pass'] = array(
-        '#type' => 'password',
-        '#title' => t('Password'));
-      $form['twitter']['twitter_text'] = array(
-        '#type' => 'textfield',
-        '#title' => t('Text format'),
-        '#default_value' => $edit['twitter_text'],
-        '#description' => t('Format of the text to submit. Use !title and !url for the post title and url respectively.'));
-      return $form;
-    case 'insert':
-    case 'update':
-      if (!empty($edit['twitter_user']) && !empty($edit['twitter_pass'])) {
-        $edit['twitter_encrypted'] = base64_encode($edit['twitter_user'] .':'. $edit['twitter_pass']);
+
+function twitter_fetch_user_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://twitter.com/statuses/user_timeline/$screen_name.xml";
+
+  if (!empty($since)) {
+    $url .= '?since='. urlencode($since);
+  }
+
+  $results = drupal_http_request($url, array(), 'GET');
+  if ($results->code == 304) {
+    return array();
+  }
+  else {
+    $results = _twitter_convert_xml_to_array($results->data);
+    if ($cache) {
+      foreach($results as $status) {
+        twitter_cache_status($status);
       }
-      unset($edit['twitter_pass']);
+    }
+    return $results;
   }
 }
 
-function twitter_nodeapi(&$node, $op) {
-  switch ($op) {
-    case 'insert':
-      global $user;
-      if ($node->status == 1) {
-        twitter_post($node, $user);
-      }
+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);
+}
+
+function twitter_fetch_user_followers($screen_name, $password, $cache = TRUE) {
+  $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');
+  $results = _twitter_convert_xml_to_array($results->data);
+
+  if ($cache) {
+    foreach($results as $status) {
+      twitter_cache_status($status);
+    }
   }
+  return $results;
 }
 
-/**
- * Implements the twitter posting API per:
- * http://twitter.com/help/api
- */
-function twitter_post($node, $account) {
-  if (empty($account->twitter_encrypted)) {
-    return false;
+function twitter_fetch_status($screen_name, $cache = TRUE) {
+  $url = "http://twitter.com/statuses/$screen_name.xml";
+  $results = drupal_http_request($url, array(), 'GET');
+  $results = _twitter_convert_xml_to_array($results->data);
+
+  if ($cache && !empty($results)) {
+    foreach($results as $status) {
+      twitter_cache_status($status);
+      return $status;
+    }
   }
+}
+
+function twitter_authenticate($screen_name, $password) {
+  $url = "http://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://twitter.com/account/end_session', $headers, 'GET');
+  return ($results->code == '200');
+}
 
-  $text = ($account->twitter_text) ? $account->twitter_text : 'New post: !title (!url)';
-  $text = t($text, array('!title' => $node->title,
-                         '!url' => url('node/' . $node->nid, NULL, NULL, TRUE)));
-  
-  $headers = array('Authorization' => 'Basic '. $account->twitter_encrypted,
+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);
+
+  if ($cache) {
+    foreach($results as $user) {
+      twitter_cache_user($user);
+    }
+  }
+  return $results;
+}
+
+function twitter_cache_status($status = array(), $silent = FALSE) {
+  dsm($status);
+  db_query("DELETE FROM {twitter} WHERE twitter_id = %d", $status['twitter_id']);
+  $sql  = "INSERT INTO {twitter} (twitter_id, screen_name, created_at, created_time, `text`, source) ";
+  $sql .= "VALUES (%d, '%s', '%s', %d, '%s', '%s')";
+  db_query($sql, $status['twitter_id'], $status['screen_name'], $status['created_at'], $status['created_time'], $status['text'], $status['source']);
+  if (!$silent) {
+    module_invoke_all('twitter_status_update', $status);
+  }
+}
+
+function twitter_cache_user($twitter_user = array()) {
+  db_query("DELETE FROM {twitter_account} WHERE twitter_uid = %d", $twitter_user['twitter_uid']);
+  $sql  = "INSERT INTO {twitter_account} (twitter_uid, name, screen_name, description, profile_image_url, url, protected) ";
+  $sql .= "VALUES (%d, '%s', '%s', '%s', '%s', '%s', %d)";
+}
+
+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);
+}
+
+/**
+ * Internal XML munging code
+ */
+
+function _twitter_convert_xml_to_array($data) {
+  $results = array();
+  $xml = new SimpleXMLElement($data);
+  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']);
+  return $result;
+}
 
-  $result = drupal_http_request(TWITTER_URL, $headers, 'POST', $data);
-  drupal_set_message(t('Posted to twitter.com'));
-}
\ No newline at end of file
+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']);
+  }
+  else {
+    $result['twitter_uid'] = NULL;
+  }
+  return $result;
+}
Index: twitter.schema
===================================================================
RCS file: twitter.schema
diff -N twitter.schema
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter.schema	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,32 @@
+<?php
+// $Id$
+
+function twitter_schema() {
+  $schema['twitter'] = array(
+    'fields' => array(
+      'twitter_id'    => array('type' => 'int', 'not null' => TRUE),
+      'screen_name'   => array('type' => 'varchar', 'length' => 255),
+      'created_at'    => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => ''),
+      'created_time'  => array('type' => 'int', 'not null' => TRUE),
+      'text'          => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE),
+      'source'        => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE),
+    ),
+    'indexes' => array('screen_name' => array('screen_name')),
+    'primary key' => array('twitter_id'),
+  );
+
+  $schema['twitter_account'] = array(
+    'fields' => array(
+      'twitter_uid'       => array('type' => 'int', 'not null' => TRUE),
+      'name'              => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => ''),
+      'screen_name'       => array('type' => 'varchar', 'length' => 255),
+      'description'       => array('type' => 'varchar', 'length' => 255),
+      'profile_image_url' => array('type' => 'varchar', 'length' => 255),
+      'url'               => array('type' => 'varchar', 'length' => 255),
+      'protected'         => array('type' => 'int', 'not null' => FALSE, 'default' => 0),
+    ),
+    'primary key' => array('screen_name'),
+  );
+
+  return $schema;
+}
\ No newline at end of file
Index: twitter_actions.info
===================================================================
RCS file: twitter_actions.info
diff -N twitter_actions.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_actions.info	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,6 @@
+; $Id$
+name = Twitter actions
+description = Exposes Drupal actions to send Twitter messages.
+dependencies[] = twitter
+package = Twitter
+core = 6.x
Index: twitter_actions.module
===================================================================
RCS file: twitter_actions.module
diff -N twitter_actions.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_actions.module	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,175 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Exposes Drupal actions for sending Twitter messages.
+ */
+
+/**
+ * Implementation of hook_action_info().
+ */
+function twitter_actions_action_info() {
+  return array(
+    'twitter_actions_set_status_action' => array(
+      'type' => 'system',
+      'description' => t('Post a message to Twitter'),
+      'configurable' => TRUE,
+      'hooks' => array(
+        'nodeapi' => array('view', 'insert', 'update', 'delete'),
+        'comment' => array('view', 'insert', 'update', 'delete'),
+        'user' => array('view', 'insert', 'update', 'delete', 'login'),
+        'cron' => array('run'),
+      ),
+    ),
+  );
+}
+
+/**
+ * Return a form definition so the Send email action can be configured.
+ *
+ * @param $context
+ *   Default values (if we are editing an existing action instance).
+ * @return
+ *   Form definition.
+ */
+function twitter_actions_set_status_action_form($context = array()) {
+  // Set default values for form.
+  $context += array(
+    'screen_name' => '',
+    'password' => '',
+    'message' => '',
+  );
+
+  $form['screen_name'] = array(
+    '#type'          => 'textfield',
+    '#title'         => t('Twitter account name'),
+    '#default_value' => $context['screen_name'],
+    '#size'          => 25,
+    '#required'      => TRUE,
+  );
+
+  $form['password'] = array(
+    '#title'         => t('Twitter password'),
+    '#type'          => 'password',
+    '#size'          => 25,
+    '#required'      => TRUE,
+  );
+
+  $form['message'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Message'),
+    '#default_value' => $context['message'],
+    '#cols' => '80',
+    '#rows' => '3',
+    '#description' => t('The message that should be sent. You may include the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body. Not all variables will be available in all contexts.'),
+    '#required'      => TRUE,
+  );
+  return $form;
+}
+
+function twitter_actions_set_status_action_validate($form, $form_state) {
+  $verify = FALSE;
+
+  $pass = $form_state['values']['password'];
+  $name = $form_state['values']['screen_name'];
+
+  $valid = twitter_authenticate($name, $pass);
+  if (!$valid) {
+    form_set_error('password', t('Twitter authentication failed. Please check your account name and try again.'));
+  }
+}
+
+function twitter_actions_set_status_action_submit($form, $form_state) {
+  $form_values = $form_state['values'];
+  // Process the HTML form to store configuration. The keyed array that
+  // we return will be serialized to the database.
+  $params = array(
+    'screen_name' => $form_values['screen_name'],
+    'password'   => $form_values['password'],
+    'message'   => $form_values['message'],
+  );
+  return $params;
+}
+
+/**
+ * Implementation of a configurable Drupal action.
+ * Sends an email.
+ */
+function twitter_actions_set_status_action($object, $context) {
+  global $user;
+  $variables['%site_name'] = variable_get('site_name', 'Drupal');
+
+  switch ($context['hook']) {
+    case 'nodeapi':
+      // Because this is not an action of type 'node' the node
+      // will not be passed as $object, but it will still be available
+      // in $context.
+      $node = $context['node'];
+      break;
+    // The comment hook provides nid, in $context.
+    case 'comment':
+      $comment = $context['comment'];
+      $node = node_load($comment->nid);
+    case 'user':
+      // Because this is not an action of type 'user' the user
+      // object is not passed as $object, but it will still be available
+      // in $context.
+      $account = $context['account'];
+      if (isset($context['node'])) {
+        $node = $context['node'];
+      }
+      elseif ($context['recipient'] == '%author') {
+        // If we don't have a node, we don't have a node author.
+        watchdog('error', 'Cannot use %author token in this context.');
+        return;
+      }
+      break;
+    case 'taxonomy':
+      $account = $user;
+      $vocabulary = taxonomy_vocabulary_load($object->vid);
+      $variables = array_merge($variables, array(
+        '%term_name' => $object->name,
+        '%term_description' => $object->description,
+        '%term_id' => $object->tid,
+        '%vocabulary_name' => $vocabulary->name,
+        '%vocabulary_description' => $vocabulary->description,
+        '%vocabulary_id' => $vocabulary->vid,
+        )
+      );
+      break;
+    default:
+      // We are being called directly.
+      $node = $object;
+  }
+
+  $from = variable_get('site_mail', ini_get('sendmail_from'));
+  $recipient = $context['recipient'];
+
+  if (isset($node)) {
+    if (!isset($account)) {
+      $account = user_load(array('uid' => $node->uid));
+    }
+    if ($recipient == '%author') {
+      $recipient = $account->mail;
+    }
+  }
+
+  $variables['%username'] = $account->name;
+
+  // Node-based variable translation is only available if we have a node.
+  if (isset($node) && is_object($node)) {
+    $variables = array_merge($variables, array(
+        '%uid' => $node->uid,
+        '%node_url' => url('node/'. $node->nid, array('absolute' => TRUE)),
+        '%node_type' => node_get_types('name', $node),
+        '%title' => $node->title,
+        '%teaser' => $node->teaser,
+        '%body' => $node->body
+      )
+    );
+  }
+
+  $message = strtr($context['message'], $variables);
+  twitter_set_status($user_name, $password, $message);
+}
Index: twitter_integration.info
===================================================================
RCS file: twitter_integration.info
diff -N twitter_integration.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_integration.info	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,6 @@
+; $Id$
+name = Twitter integration
+description = Connects Twitter data to Drupal users and content.
+dependencies[] = twitter
+package = Twitter
+core = 6.x
Index: twitter_integration.install
===================================================================
RCS file: twitter_integration.install
diff -N twitter_integration.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_integration.install	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,14 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function twitter_integration_install() {
+  // Create tables.
+  drupal_install_schema('twitter_integration');
+}
+
+function twitter_integration_uninstall() {
+  drupal_uninstall_schema('twitter_integration');
+}
\ No newline at end of file
Index: twitter_integration.module
===================================================================
RCS file: twitter_integration.module
diff -N twitter_integration.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_integration.module	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,561 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Manages Twitter connections to Drupal users and nodes
+ */
+
+/**
+ * Hooks into the core Drupal system. Menus, permissions, cron, etc...,
+ * etc...
+ */
+
+function twitter_integration_perm() {
+  return array('use twitter account', 'post to twitter', 'import twitter statuses', 'view twitter statuses');
+}
+
+function twitter_menu() {
+  $items = array();
+  $items['user/%user/twitter'] = array(
+    'title' => 'Twitter status',
+    'page callback' => 'twitter_integration_user_status_page',
+    'page arguments' => array(1),
+    'access callback' => 'user_access',
+    'access arguments' => array('view twitter statuses'),
+    'weight' => 10,
+    'type' => MENU_LOCAL_TASK,
+  );
+
+  $items['user/%user/edit/twitter'] = array(
+    'title' => 'Twitter settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('twitter_integration_user_settings', 1),
+    'access callback' => 'twitter_integration_edit_access',
+    'access arguments' => array(1),
+    'weight' => 10,
+    'type' => MENU_LOCAL_TASK,
+  );
+
+  return $items;
+}
+
+function twitter_integration_edit_access($account) {
+  return (($GLOBALS['user']->uid == $account->uid) && user_access('use twitter account')) || user_access('administer users');
+}
+
+function twitter_integration_theme() {
+  return array(
+    'twitter_integration_status' => array(
+      'arguments' => array('screen_name' => '', 'text' => '', 'format' => 'short', 'status' => array(), $inline = FALSE),
+    ),
+    'twitter_integration_user_status_page' => array(
+      'arguments' => array('user' => NULL, 'statues' => array()),
+    ),
+  );
+}
+
+function twitter_integration_nodeapi(&$node, $op) {
+  switch ($op) {
+    case 'insert':
+      $twitter_accounts = twitter_integration_account_load($node->uid);
+      if (!empty($twitter_accounts)) {
+        $twitter = array_pop($twitter_accounts);
+        if ($node->status == 1 && in_array($node->type, $twitter['ping_types'])) {
+          $message = t($twitter['ping_message'], array('!title' => $node->title, '!url' => url('node/' . $node->nid, array('absolute' => TRUE))));
+          twitter_set_status($twitter['sreen_name'], $twitter['password'], $message);
+        }
+      }
+      break;
+    case 'delete':
+      db_query("DELETE FROM {twitter_node} WHERE nid = %d", $node->nid);
+      break;
+    case 'view':
+      $statuses = twitter_integration_get_node_statuses($node->nid);
+      foreach ($statuses as $status) {
+        $node->content['twitter'][] = array(
+          '#value' => theme('twitter_integration_status', $status['screen_name'], $status['text'], 'timed', $status),
+        );
+      }
+      break;
+  }
+}
+
+function twitter_integration_block($op = 'list', $delta = 0, $edit = array()) {
+  if ($op == 'list') {
+     $blocks[0]['info'] = t('Latest tweets');
+     $blocks[1]['info'] = t('Author tweets');
+     return $blocks;
+  }
+  else if ($op == 'view' && user_access('view twitter statuses')) {
+    switch ($delta) {
+      case 0:
+        $output = '';
+        $account = NULL;
+        if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
+          $node = node_load(arg(1));
+          $account = user_load(array('uid' => $node->uid));
+        }
+        elseif (((arg(0) == 'blog' || arg(0) == 'user') && is_numeric(arg(1)))) {
+          $account = user_load(array('uid' => arg(1)));
+        }
+
+        $items = array();
+        if (!empty($account->uid)) {
+          $output = '';
+          $sql = "SELECT t.* FROM {twitter} t INNER JOIN {twitter_user} tu ";
+          $sql .= "ON t.screen_name = tu.screen_name WHERE tu.show_in_blocks = 1 AND tu.uid = %d ";
+          $sql .= "ORDER BY t.created_time DESC ";
+          $results = db_query_range($sql, array($account->uid), 1, 5);
+
+          while ($status = db_fetch_array($results)) {
+            $items[] = theme('twitter_integration_status', $status['screen_name'], $status['text'], 'short', $status, TRUE);
+          }
+        }
+
+        if (!empty($items)) {
+           $block['subject'] = t('!name\'s tweets', array('!name' => $account->name));
+           $block['content'] = theme('item_list', $items);
+           return $block;
+        }
+        break;
+      case 1:
+        $output = '';
+        $sql = "SELECT t.* FROM {twitter} t INNER JOIN {twitter_user} tu ";
+        $sql .= "ON t.screen_name = tu.screen_name WHERE tu.show_in_blocks = 1 ";
+        $sql .= "ORDER BY t.created_time DESC ";
+        $results = db_query_range($sql, array(), 1, 5);
+
+        $items = array();
+        while ($status = db_fetch_array($results)) {
+          $items[] = theme('twitter_integration_status', $status['screen_name'], $status['text'], 'named', $status, TRUE);
+        }
+
+        if (!empty($items)) {
+           $block['subject'] = t('Latest tweets');
+           $block['content'] = theme('item_list', $items);
+           return $block;
+        }
+        break;
+    }
+  }
+}
+
+function twitter_integration_cron() {
+  $cron_interval = variable_get('twitter_integration_cron_interval', 3600);
+  $last_cron = variable_get('twitter_integration_last_cron', 0);
+  $current_cron = time();
+
+  $sql = "SELECT * FROM {twitter_user} tu WHERE tu.last_refresh < %d";
+
+  // We'll only process 20 users at a time to avoid hammering Twitter.
+  $result = db_query_range($sql, array(time() - $cron_interval), 1, 20);
+  while ($account = db_fetch_array($result)) {
+
+    // First refresh the local tweets
+    twitter_fetch_user_timeline($account['screen_name']);
+
+    if ($account['node_interval'] + $account['node_last'] < $current_cron) {
+      _twitter_integration_make_node($account);
+    }
+
+    $account['last_refresh'] = time();
+    twitter_integration_user_save($account);
+  }
+
+  variable_set('twitter_integration_last_cron', $current_cron);
+}
+
+function _twitter_integration_make_node(&$account = array()) {
+  $account += array(
+    'last_refresh'      => 0,
+    'node_last'         => 0,
+    'node_interval'     => -1,
+    'node_type'         => '',
+    'node_title'        => '',
+  );
+
+  $last = ($account['node_last'] > 0) ? $account['node_last'] : time() - $account['node_interval'];
+  $sql = "SELECT t.twitter_id FROM {twitter} t LEFT JOIN {twitter_node} tn ON t.twitter_id = tn.twitter_id WHERE t.screen_name = '%s' AND t.created_time >= %d AND t.created_time < %d AND tn.nid IS NULL";
+  $results = db_query($sql, $account['screen_name'], $last, $last + $account['node_interval']);
+
+  $tids = array();
+  foreach (db_fetch_array($results) as $result) {
+    $tids[] = $result['twitter_id'];
+  }
+
+  if (!empty($tids)) {
+    $form_state = array();
+    $node = array('type' => $account['node_type']);
+    $form_state['values']['title'] = $account['node_title'];
+    $form_state['values']['uid'] = $account['uid'];
+    drupal_execute($account['node_type'] .'_node_form', $form_state, $node);
+    $nid = $form_state['nid'];
+    foreach ($tids as $tid) {
+      twitter_integration_attach_to_node($nid, $tid);
+    }
+  }
+  $account['node_last'] = $last;
+}
+
+
+/**
+ * Page callbacks and shared theming functions
+ */
+
+function twitter_integration_user_settings($form_state, $user) {
+  $twitter_accounts = twitter_integration_account_load($user->uid);
+  if (!empty($twitter_accounts)) {
+    $twitter = array_pop($twitter_accounts);
+  }
+  else {
+    $twitter = array();
+  }
+
+  $form['uid'] = array(
+    '#type' => 'value',
+    '#value' => $user->uid,
+  );
+  $form['screen_name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Twitter user name'),
+    '#default_value' => !empty($twitter) ? $twitter['screen_name'] : '',
+  );
+  $form['password'] = array(
+    '#type' => 'password',
+    '#title' => t('Password'),
+    '#default_value' => !empty($twitter) ? $twitter['password'] : '',
+  );
+
+  $form['show_in_blocks'] = array (
+    '#type' => 'checkbox',
+    '#title' => t('Display my status in a sidebar block'),
+    '#default_value' => !empty($twitter) ? $twitter['show_in_blocks'] : TRUE,
+  );
+
+  $form['show_on_profile'] = array (
+    '#type' => 'checkbox',
+    '#title' => t('Display my status on my profile page'),
+    '#default_value' => !empty($twitter) ? $twitter['show_on_profile'] : TRUE,
+  );
+
+  $form['import'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Twitter import'),
+    '#description' => t('Optionally import Twitter updates and aggregate them into regular blog posts or other content.'),
+    '#weight' => 4,
+    '#access' => user_access('import twitter statuses'),
+  );
+
+  $options = array(-1 => t('Never'));
+  $intervals = drupal_map_assoc(array(14400, 28800, 86400, 604800), 'format_interval');
+  $options = array_merge($options, $intervals);
+  $form['import']['node_interval'] = array (
+    '#type' => 'select',
+    '#title' => t('Create a Twitter node every'),
+    '#options' => $options,
+    '#default_value' => !empty($twitter) ? $twitter['node_interval'] : 86400,
+  );
+
+  $options = node_get_types('names');
+  foreach ($options as $type => $option) {
+    if (node_access('view', $type)) {
+      $types[$type] = $option;
+    }
+  }
+
+  $form['import']['node_type'] = array (
+    '#type' => 'select',
+    '#title' => t('Node type for Twitter statuses'),
+    '#options' => $types,
+    '#default_value' => !empty($twitter['node_type']) ? $twitter['node_type'] : 'story',
+  );
+
+  $form['import']['node_title'] = array (
+    '#type' => 'textfield',
+    '#title' => t('Title for Twitter nodes'),
+    '#default_value' => !empty($twitter['node_title']) ? $twitter['node_title'] : t("Today's tweets..."),
+  );
+
+  $form['ping'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Twitter announcements'),
+    '#description' => t('Optionally ping Twitter when you post new content.'),
+    '#weight' => 4,
+    '#access' => user_access('post to twitter'),
+  );
+
+  $options = node_get_types('names');
+  foreach ($options as $type => $option) {
+    if (node_access('view', $type)) {
+      $types[$type] = $option;
+    }
+  }
+
+  $defaults = array();
+  if (!empty($twitter)) {
+    foreach($twitter['ping_types'] as $type) {
+      $defaults[$type] = $type;
+    }
+  }
+
+  $form['ping']['ping_types'] = array (
+    '#type' => 'select',
+    '#title' => t('Node type to announce'),
+    '#options' => $types,
+    '#multiple' => TRUE,
+    '#default_value' => $defaults,
+  );
+
+  $form['ping']['ping_message'] = array (
+    '#type' => 'textarea',
+    '#title' => t('Message text'),
+    '#rows' => 2,
+    '#default_value' => !empty($twitter['ping_message']) ? $twitter['ing_message'] : t("!title (!url)"),
+    '#description'   => t('The text to submit. Use !title and !url for the post title and url respectively.'),
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+    '#weight' => 10,
+  );
+
+  return $form;
+}
+
+function twitter_integration_user_settings_validate($form, &$form_state) {
+  if (!empty($edit['twitter'])) {
+    $verify = FALSE;
+
+    $pass = $form_state['values']['password'];
+    $name = $form_state['values']['screen_name'];
+
+    if (!empty($pass)) {
+      $verify = TRUE;
+    }
+
+    if ($verify) {
+      $valid = twitter_authenticate($name, $pass);
+      if (!$valid) {
+        form_set_error("password", t('Twitter authentication failed. Please check your account name and try again.'));
+      }
+    }
+  }
+}
+
+function twitter_integration_user_settings_submit($form, &$form_state) {
+  if (!empty($form_state['values'])) {
+    if (!empty($form_state['values']['screen_name'])) {
+      if (!user_access('import twitter statuses')) {
+        $form_state['values']['node_interval'] = -1;
+        $form_state['values']['node_type'] = NULL;
+        $form_state['values']['node_title'] = NULL;
+      }
+      if (!user_access('post to twitter')) {
+        $form_state['values']['ping_types'] = array();
+        $form_state['values']['ping_message'] = NULL;
+      }
+      $tmp = array_values($form_state['values']['ping_types']);
+      $form_state['values']['ping_types'] = array();
+      foreach ($tmp as $type) {
+        if (!empty($type)) {
+          $form_state['values']['ping_types'][] = $type;
+        }
+      }
+      twitter_integration_user_save($form_state['values']);
+    }
+    else {
+      twitter_integration_user_delete($account->uid);
+    }
+  }
+}
+
+function twitter_integration_user_status_page($user = NULL) {
+  if (empty($user)) {
+    drupal_not_found();
+    return;
+  }
+  drupal_set_title($user->name);
+
+  $accounts = twitter_integration_account_load($user->uid);
+  $names = array_keys($accounts);
+  $grouped = twitter_integration_get_account_statuses($names, FALSE, 20);
+
+  $results = array();
+  foreach ($grouped as $name => $statuses) {
+    foreach($statuses as $status) {
+      $results[] = $status;
+    }
+  }
+
+  return theme('twitter_integration_user_status_page', $user, $results);
+}
+
+function theme_twitter_integration_user_status_page($user, $statuses = array()) {
+  $output = '';
+  if (!empty($statuses)) {
+    $current = array_shift($statuses);
+    $output .= '<h1>'. check_plain($current['text']) .'</h1>';
+  }
+  foreach ($statuses as $status) {
+    $output .= theme('twitter_integration_status', $status['screen_name'], $status['text'], 'timed', $status);
+  }
+  return $output;
+}
+
+function theme_twitter_integration_status($screen_name, $text, $format = 'short', $status = array(), $inline = FALSE) {
+  switch ($format) {
+    case 'short':
+      $output = check_plain($text);
+      break;
+    case 'timed':
+      $output = check_plain($text);
+      $output .= ' <span class="twitter-timestamp">'. l(format_date($status['created_time'], 'small'), 'http://www.twitter.com/statuses/'. $status['twitter_id'], array('absolute' => TRUE, 'html' => TRUE)) .'</span>';
+      break;
+    case 'named':
+      $output = l($screen_name, 'http://www.twitter.com/'. $screen_name, array('absolute' => TRUE)) .': ';
+      $output .= check_plain($text);
+      break;
+    case 'full':
+      $output = l($screen_name, 'http://www.twitter.com/'. $screen_name, array('absolute' => TRUE)) .': ';
+      $output .= check_plain($text);
+      $output .= ' <span class="twitter-timestamp">'. l(format_date($status['created_time'], 'small'), 'http://www.twitter.com/statuses/'. $status['twitter_id'], array('absolute' => TRUE, 'html' => TRUE)) .'</span>';
+      break;
+    default:
+      $output = check_plain($text);
+      break;
+  }
+
+  if (!$inline) {
+    $output = '<div class="twitter-status">'. $output .'</div>';
+  }
+  return $output;
+}
+
+
+/**
+ * CRUD and connection management functions for the Twitter-to-node and
+ * Twitter-account-to-Drupal-user mapping.
+ */
+
+function twitter_integration_get_node_statuses($nid, $module = NULL) {
+  $sql = "SELECT t.* FROM {twitter} t LEFT JOIN {twitter_integration} tn ON t.twitter_id = tn.twitter_id WHERE tn.nid = %d";
+  $args = array($nid);
+  if (!empty($module)) {
+    $sql .= " AND tn.module = '%s'";
+    $args[] = $module;
+  }
+  $results = db_query($sql, $args);
+
+  $statuses = array();
+  while ($status = db_fetch_array($results)) {
+    $statuses[$status['twitter_id']] = $status;
+  }
+  return $statuses;
+}
+
+function twitter_integration_get_account_statuses($screen_names = array(), $refresh = FALSE, $limit = 10) {
+  if (empty($screen_names)) {
+    return array();
+  }
+
+  if ($refresh) {
+    foreach ($screen_names as $name) {
+      twitter_fetch_user_timeline($name);
+    }
+  }
+
+  if (!is_array($screen_names)) {
+    $screen_names = array($screen_names);
+  }
+  $sql  = "SELECT t.* FROM {twitter} t WHERE t.screen_name IN (";
+  $sql .= implode(",", array_fill(0, count($screen_names), "'%s'"));
+  $sql .= ") ORDER BY t.created_time DESC";
+
+  if (empty($limit)) {
+    $results = db_query($sql, $screen_names);
+  }
+  else {
+    $results = db_query_range($sql, $screen_names, 1, $limit);
+  }
+
+  $statuses = array();
+  while ($status = db_fetch_array($results)) {
+    $statuses[$status['screen_name']][$status['twitter_id']] = $status;
+  }
+  return $statuses;
+}
+
+function twitter_integration_attach_to_node($nid, $twitter_id, $module = 'twitter_integration') {
+  twitter_detach_from_node($nid, $twitter_id);
+  db_query("INSERT INTO {twitter_integration} tn (nid, twitter_id, module) VALUES (%d, %d, '%s')", $nid, $twitter_id, $module);
+}
+
+function twitter_integration_detach_from_node($nid, $twitter_id) {
+  db_query("DELETE FROM {twitter_integration} WHERE nid = %d AND twitter_id = %d", $nid, $twitter_id);
+}
+
+function twitter_integration_user_save($user = array()) {
+  $user += array(
+    'screen_name'       => '',
+    'password'          => '',
+    'last_refresh'      => 0,
+    'show_on_profile'   => 1,
+    'show_in_blocks'    => 1,
+    'node_interval'     => -1,
+    'node_last'         => 0,
+    'node_type'         => '',
+    'node_title'        => '',
+    'ping_types'        => array(),
+    'ping_message'      => '',
+  );
+
+  if (db_result(db_query("SELECT 1 FROM {twitter_user} WHERE uid = %d AND screen_name = '%s'", $user['uid'], $user['screen_name']))) {
+    $sql  = "UPDATE {twitter_user} SET password = '%s', last_refresh = %d, show_on_profile = %d, show_in_blocks = %d, node_interval = %d, node_last = %d, node_type = '%s', node_title = '%s', ping_types = '%s', ping_message = '%s' ";
+    $sql .= "WHERE uid = %d AND screen_name = '%s'";
+    db_query($sql, $user['password'], time(),
+             $user['show_on_profile'], $user['show_in_blocks'],
+             $user['node_interval'], $user['node_last'], $user['node_type'], $user['node_title'],
+             $user['ping_types'], $user['ping_message'],
+             $user['uid'], $user['screen_name']);
+  }
+  else {
+    // For now, we're going to nuke other user accounts.
+    db_query("DELETE FROM {twitter_user} WHERE uid = %d", $user['uid']);
+
+    $sql  = "INSERT INTO {twitter_user} (uid, screen_name, password, last_refresh, show_on_profile, show_in_blocks, node_interval, node_last, node_type, node_title, ping, ping_types, ping_message) ";
+    $sql .= "VALUES (%d, '%s', '%s', %d, %d, %d, %d, '%s', '%s', %d, '%s')";
+    db_query($sql, $user['uid'], $user['screen_name'], $user['password'], time(), $user['show_on_profile'], $user['show_in_blocks'], $user['node_interval'], $user['node_last'], $user['node_type'], $user['node_title'], $user['ping_types'], $user['ping_message']);
+  }
+  twitter_fetch_user_details($user['screen_name'], $user['password']);
+  twitter_fetch_user_timeline($user['screen_name']);
+}
+
+function twitter_integration_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'";
+    $args[] = $screen_name;
+  }
+  db_query($sql, $args);
+}
+
+function twitter_integration_account_load($uid, $screen_name = NULL) {
+  $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";
+  $args = array($uid);
+  if (!empty($screen_name)) {
+    $sql .= " AND tu.screen_name = '%s'";
+    $args[] = $screen_name;
+  }
+
+  $results = db_query($sql, $args);
+
+  $accounts = array();
+  while ($account = db_fetch_array($results)) {
+    $account['ping_types'] = unserialize($account['ping_types']);
+    $accounts[$account['screen_name']] = $account;
+  }
+  return $accounts;
+}
Index: twitter_integration.schema
===================================================================
RCS file: twitter_integration.schema
diff -N twitter_integration.schema
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_integration.schema	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,33 @@
+<?php
+// $Id$
+
+function twitter_integration_schema() {
+  $schema['twitter_node'] = array(
+    'fields' => array(
+      'nid'               => array('type' => 'int', 'not null' => TRUE),
+      'twitter_id'        => array('type' => 'int', 'not null' => TRUE),
+      'module'            => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => 'twitter_node'),
+    ),
+    'primary key' => array('nid', 'twitter_id'),
+  );
+
+  $schema['twitter_user'] = array(
+    'fields' => array(
+      'uid'               => array('type' => 'int', 'not null' => TRUE),
+      'screen_name'       => array('type' => 'varchar', 'length' => 255),
+      'password'          => array('type' => 'varchar', 'length' => 64),
+      'last_refresh'      => array('type' => 'int', 'not null' => TRUE),
+      'show_on_profile'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'show_in_blocks'    => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'node_interval'     => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'node_last'         => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'node_type'         => array('type' => 'varchar', 'length' => 64),
+      'node_title'        => array('type' => 'varchar', 'length' => 128),
+      'ping_types'        => array('type' => 'varchar', 'length' => 255),
+      'ping_message'      => array('type' => 'varchar', 'length' => 255),
+    ),
+    'primary key' => array('uid', 'screen_name'),
+  );
+
+  return $schema;
+}
Index: twitter_log.info
===================================================================
RCS file: twitter_log.info
diff -N twitter_log.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_log.info	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,6 @@
+; $Id$
+name = Twitter log
+description = Logs and records system events to Twitter.
+dependencies[] = twitter
+package = Twitter
+core = 6.x
Index: twitter_log.module
===================================================================
RCS file: twitter_log.module
diff -N twitter_log.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_log.module	29 Jul 2007 23:03:35 -0000
@@ -0,0 +1,110 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Logs and records system events to Twitter.
+ */
+
+function twitter_log_help($path, $arg) {
+  switch ($path) {
+    case 'admin/help#twitter_log':
+      return '<p>'. t('Provides the facility to log Drupal messages to a Twitter.com account. Twitter is a free microblogging site with new messages notification via SMS, email, desktop widget, etc. Because the API is a free resource, only severe issues should be directed to Twitter.') .'</p>';
+  }
+}
+
+function twitter_log_menu() {
+  $items['admin/settings/logging/twitter_log'] = array(
+    'title'          => 'Twitter log',
+    'description'    => 'Settings for Twitter logging. Twitter is a free microblogging site with new messages notification via SMS, email, desktop widget, etc. Because the API is a free resource, only severe issues should be directed to Twitter.',
+    'page callback'  => 'drupal_get_form',
+    'page arguments' => array('twitter_log_admin_settings'),
+  );
+  return $items;
+}
+
+function twitter_log_admin_settings() {
+  if ($screen_name = variable_get('twitter_log_screen_name', NULL)) {
+    $form['description'] = array(
+      '#type'   => 'markup',
+      '#value'  => t("Log messages will be sent to !url.", array('!url' => l("http://www.twitter.com/$screen_name", "http://www.twitter.com/$screen_name"))),
+    );
+  }
+
+  $form['twitter_log_screen_name'] = array(
+    '#type'          => 'textfield',
+    '#title'         => t('Twitter account name'),
+    '#default_value' => variable_get('twitter_log_screen_name', NULL),
+    '#required'      => TRUE,
+  );
+
+  $pass =  variable_get('twitter_log_password', NULL);
+  $form['twitter_log_password'] = array(
+    '#title'         => t('Twitter password'),
+    '#type'          => 'password',
+    '#size'          => 25,
+    '#required'      => empty($pass),
+  );
+
+  $options = array(
+    WATCHDOG_EMERG    => t('Emergency: system is unusable'),
+    WATCHDOG_ALERT    => t('Alert: action must be taken immediately'),
+    WATCHDOG_CRITICAL => t('Critical: critical conditions'),
+    WATCHDOG_ERROR    => t('Error: error conditions'),
+    WATCHDOG_WARNING  => t('Warning: warning conditions'),
+    WATCHDOG_NOTICE   => t('Notice: normal but significant condition'),
+    WATCHDOG_INFO     => t('Informational: informational messages'),
+    WATCHDOG_DEBUG    => t('Debug: debug-level messages'),
+  );
+
+  $form['twitter_log_severity'] = array(
+    '#type'          => 'select',
+    '#title'         => t('Severity level'),
+    '#default_value' => variable_get('twitter_log_severity', WATCHDOG_CRITICAL),
+    '#options'       => $options,
+    '#description'   => t('Watchdog messages below this severity level will not be posted. Severity levels, as defined in !rfc, allow administrators to filter alert messages. Most Drupal log messages are Errors or Notices; Twitter\'s free API should be reservered for serious messages.', array(
+      '!rfc'         => l("RFC 3164", 'http://www.faqs.org/rfcs/rfc3164.html'),
+      )),
+  );
+
+  $form = system_settings_form($form);
+  $form['#validate'][] = 'twitter_log_admin_settings_validate';
+  return $form;
+}
+
+function twitter_log_admin_settings_validate($form, &$form_state) {
+  $verify = FALSE;
+
+  $pass = $form_state['values']['twitter_log_password'];
+  $name = $form_state['values']['twitter_log_screen_name'];
+
+  if (empty($pass)) {
+    $pass = variable_get('twitter_log_password', NULL);
+    unset($form_state['values']['twitter_log_password']);
+  }
+  else {
+    $verify = TRUE;
+  }
+  
+  if (variable_get('twitter_log_screen_name', NULL) != $name) {
+    $verify = TRUE;
+  }
+  
+  if ($verify) {
+    $valid = twitter_authenticate($name, $pass);
+    if (!$valid) {
+      form_set_error('twitter_log_password', t('Twitter authentication failed. Please check your account name and try again.'));
+    }
+  }
+}
+
+function twitter_log_watchdog($entry) {
+  $severity = variable_get('twitter_log_severity', WATCHDOG_ALERT);
+  $user_name = variable_get('twitter_log_screen_name', NULL);
+  $password = variable_get('twitter_log_password', NULL);
+  $message = strip_tags(is_null($entry['variables']) ? $entry['message'] : strtr($entry['message'], $entry['variables']));
+
+  if ($entry['severity'] <= $severity && !empty($user_name) && !empty($password)) {
+    twitter_set_status($user_name, $password, $message);
+  }
+}
\ No newline at end of file
