? .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 Jan 2008 18:31:33 -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 Jan 2008 18:31:33 -0000
@@ -0,0 +1,118 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_schema).
+ */
+function twitter_schema() {
+  $schema['twitter'] = array(
+    'description' => t("Stores individual Twitter posts."),
+    'fields' => array(
+      'twitter_id' => array(
+        'description' => t("Unique identifier for each {twitter} post."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'screen_name' => array(
+        'description' => t("Screen name of the {twitter} user."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'created_at' => array(
+        'description' => t("Date and time the {twitter} post was created."),
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => ''
+      ),
+      'created_time' => array(
+        'description' => t("A duplicate of {twitter}.created_at in UNIX timestamp format."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'text' => array(
+        'description' => t("The text of the {twitter} post."),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE
+      ),
+      'source' => array(
+        'description' => t("The application that created the {twitter} post."),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE
+      ),
+    ),
+    'indexes' => array('screen_name' => array('screen_name')),
+    'primary key' => array('twitter_id'),
+  );
+
+  $schema['twitter_account'] = array(
+    'description' => t("Stores information on specific Twitter user accounts."),
+    'fields' => array(
+      'twitter_uid' => array(
+        'description' => t("The unique identifier of the {twitter_account}."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'name' => array(
+        'description' => t("The unique login 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."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'description' => array(
+        'description' => t("The description/biography associated with the {twitter_account}."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'profile_image_url' => array(
+        'description' => t("The url of the {twitter_account}'s profile image."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'url' => array(
+        'description' => t("The url of the {twitter_account}'s home page."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'protected' => array(
+        'description' => t("Boolean flag indicating whether the {twitter_account}'s posts are publically accessible."),
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0
+      ),
+    ),
+    'indexes' => array('screen_name' => array('screen_name')),
+    'primary key' => array('twitter_uid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * 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 Jan 2008 18:31:33 -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) {
+  db_query("DELETE FROM {twitter} WHERE twitter_id = %d", $status['twitter_id']);
+  drupal_write_record('twitter', $status);
+  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']);
+  drupal_write_record('twitter_account', $twitter_user);
+}
+
+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->name)) {
+    // Top-level user information.
+    $results[] = _twitter_convert_user($xml);
+    return $results;
+  }
+  if (!empty($xml->user)) {
+    foreach($xml->user as $user) {
+      $results[] = _twitter_convert_user($user);
+    }
+  }
+  elseif (!empty($xml->status)) {
+    foreach($xml->status as $status) {
+      $results[] = _twitter_convert_status($status);
+    }
+  }
+  return $results;
+}
+
+function _twitter_convert_status($status) {
+  $result = (array)$status;
+  $result['twitter_id'] = $result['id'];
+  if (!empty($result['user']) && is_object($result['user'])) {
+    $result['account'] = _twitter_convert_user($result['user']);
+    $result['screen_name'] = $result['account']['screen_name'];
+  }
+  else {
+    $result['screen_name'] = NULL;
+  }
+  $result['created_time'] = strtotime($result['created_at']);
+  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_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 Jan 2008 18:31:33 -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 Jan 2008 18:31:34 -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 Jan 2008 18:31:34 -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 Jan 2008 18:31:34 -0000
@@ -0,0 +1,90 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function twitter_integration_install() {
+  // Create tables.
+  drupal_install_schema('twitter_integration');
+}
+
+function twitter_integration_schema() {
+  $schema['twitter_node'] = array(
+    'fields' => array(
+      'nid' => array(
+        'description' => t("The Drupal node ID that's connected to a given Twitter post."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'twitter_id' => array(
+        'description' => t("The Twitter post ID that's connected to a given Drupal node."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'module' => array(
+        'description' => t("The name of the module that linked the post to the node."),
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => 'twitter_node'
+      ),
+    ),
+    'indexes' => array('twitter_nid' => array('nid')),
+    'primary key' => array('nid', 'twitter_id'),
+  );
+
+  $schema['twitter_user'] = array(
+    'fields' => array(
+      'uid' => array(
+        'description' => t("The Drupal ID of the user account associated with the Twitter account."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'screen_name' => array(
+        'description' => t("The unique login name for the Twitter account."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'password' => array(
+        'description' => t("The password for the Twitter account."),
+        'type' => 'varchar',
+        'length' => 64
+      ),
+      'last_refresh' => array(
+        'description' => t("A UNIX timestamp marking the date Twitter statuses were last fetched on."),
+        'type' => 'int',
+        'not null' => TRUE
+      ),
+      'show_on_profile' => array(
+        'description' => t("A boolean flag indicating that the account's Twitter statuses should be shown in the user's profile."),
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0
+      ),
+      'show_in_blocks' => array(
+        'description' => t("A boolean flag indicating that the account's Twitter statuses should be shown on sitewide sidebar blocks."),
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0
+      ),
+      'ping_types' => array(
+        'description' => t("Node types for which Twitter status updates should be posted."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+      'ping_message' => array(
+        'description' => t("The message to use when posting node announcements to Twitter."),
+        'type' => 'varchar',
+        'length' => 255
+      ),
+    ),
+    'primary key' => array('uid', 'screen_name'),
+  );
+
+  return $schema;
+}
+
+function twitter_integration_uninstall() {
+  drupal_uninstall_schema('twitter_integration');
+}
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 Jan 2008 18:31:34 -0000
@@ -0,0 +1,294 @@
+<?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_integration_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,
+    'file' => 'twitter_integration.pages.inc',
+    '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,
+    'file' => 'twitter_integration.pages.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+
+  return $items;
+}
+
+function twitter_integration_edit_access($account) {
+  global $user;
+  return (($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,
+        'status' => array(),
+        'file' => 'twitter_integration.pages.inc',
+      ),
+    ),
+  );
+}
+
+function twitter_integration_nodeapi(&$node, $op) {
+  switch ($op) {
+    case 'insert':
+      $twitter_accounts = twitter_integration_account_load($node->uid);
+      foreach($twitter_accounts as $twitter) {
+        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['screen_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), 0, 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(), 0, 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() {
+  // Throttle to half an hour. Just keeps the module from thrashing Twitter
+  // on sites with rapid cron cycles.
+  $cron_interval = variable_get('twitter_integration_cron_interval', 1800);
+  $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), 0, 20);
+  while ($account = db_fetch_array($result)) {
+
+    // First refresh the local tweets
+    twitter_fetch_user_timeline($account['screen_name']);
+
+    $account['last_refresh'] = time();
+    twitter_integration_user_save($account);
+  }
+
+  variable_set('twitter_integration_last_cron', $current_cron);
+}
+
+/**
+ * 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_node} 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, 0, $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);
+  $record = array('nid' => $nid, 'twitter_id' => $twitter_id, 'module' => $module);
+  drupal_write_record('twitter_node', $record);
+}
+
+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,
+    'ping_types'        => array(),
+    'ping_message'      => '',
+  );
+
+  $user['ping_types'] = serialize($user['ping_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 {
+    // For now, we're going to nuke other user accounts.
+    db_query("DELETE FROM {twitter_user} WHERE uid = %d", $user['uid']);
+    drupal_write_record('twitter_user', $user);
+  }
+  $user['ping_types'] = unserialize($user['ping_types']);
+
+  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.pages.inc
===================================================================
RCS file: twitter_integration.pages.inc
diff -N twitter_integration.pages.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ twitter_integration.pages.inc	29 Jan 2008 18:31:34 -0000
@@ -0,0 +1,203 @@
+<?php
+// $Id$
+
+/**
+ * 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',
+    '#required' => TRUE,
+    '#title' => t('Twitter user name'),
+    '#default_value' => !empty($twitter) ? $twitter['screen_name'] : '',
+  );
+  $form['password'] = array(
+    '#type' => 'password',
+    '#required' => TRUE,
+    '#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['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');
+  $types = array();
+  foreach ($options as $type => $option) {
+    if (node_access('create', $type)) {
+      $types[$type] = $option;
+    }
+  }
+
+  $defaults = array();
+  if (!empty($twitter) && !empty($twitter['ping_types'])) {
+    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['ping_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>';
+  }
+  $list = array();
+  foreach ($statuses as $status) {
+    $list[] = theme('twitter_integration_status', $status['screen_name'], $status['text'], 'timed', $status);
+  }
+  $output .= theme('item_list', $list);
+  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_interval(time() - $status['created_time']), 'http://www.twitter.com/'. $status['screen_name'] .'/statuses/'. $status['twitter_id'], array('absolute' => TRUE, 'html' => TRUE)) .' ago)</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/'. $status['screen_name'] .'/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;
+}
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 Jan 2008 18:31:34 -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 Jan 2008 18:31:34 -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
