web/wp-content/plugins/twitter-tools/twitter-tools.php
changeset 136 bde1974c263b
child 194 32102edaa81b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/wp-content/plugins/twitter-tools/twitter-tools.php	Wed Feb 03 15:37:20 2010 +0000
@@ -0,0 +1,2730 @@
+<?php
+/*
+Plugin Name: Twitter Tools
+Plugin URI: http://alexking.org/projects/wordpress
+Description: A complete integration between your WordPress blog and <a href="http://twitter.com">Twitter</a>. Bring your tweets into your blog and pass your blog posts to Twitter. Show your tweets in your sidebar, and post tweets from your WordPress admin.
+Version: 2.0
+Author: Alex King
+Author URI: http://alexking.org
+*/
+
+// Copyright (c) 2007-2009 Crowd Favorite, Ltd., Alex King. All rights reserved.
+//
+// Released under the GPL license
+// http://www.opensource.org/licenses/gpl-license.php
+//
+// This is an add-on for WordPress
+// http://wordpress.org/
+//
+// Thanks to John Ford ( http://www.aldenta.com ) for his contributions.
+// Thanks to Dougal Campbell ( http://dougal.gunters.org ) for his contributions.
+// Thanks to Silas Sewell ( http://silas.sewell.ch ) for his contributions.
+// Thanks to Greg Grubbs for his contributions.
+//
+// **********************************************************************
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
+// **********************************************************************
+
+load_plugin_textdomain('twitter-tools');
+
+if (!defined('PLUGINDIR')) {
+	define('PLUGINDIR','wp-content/plugins');
+}
+
+if (is_file(trailingslashit(ABSPATH.PLUGINDIR).'twitter-tools.php')) {
+	define('AKTT_FILE', trailingslashit(ABSPATH.PLUGINDIR).'twitter-tools.php');
+}
+else if (is_file(trailingslashit(ABSPATH.PLUGINDIR).'twitter-tools/twitter-tools.php')) {
+	define('AKTT_FILE', trailingslashit(ABSPATH.PLUGINDIR).'twitter-tools/twitter-tools.php');
+}
+
+if (!function_exists('wp_prototype_before_jquery')) {
+	function wp_prototype_before_jquery( $js_array ) {
+		if ( false === $jquery = array_search( 'jquery', $js_array ) )
+			return $js_array;
+	
+		if ( false === $prototype = array_search( 'prototype', $js_array ) )
+			return $js_array;
+	
+		if ( $prototype < $jquery )
+			return $js_array;
+	
+		unset($js_array[$prototype]);
+	
+		array_splice( $js_array, $jquery, 0, 'prototype' );
+	
+		return $js_array;
+	}
+	
+	add_filter( 'print_scripts_array', 'wp_prototype_before_jquery' );
+}
+
+define('AKTT_API_POST_STATUS', 'http://twitter.com/statuses/update.json');
+define('AKTT_API_USER_TIMELINE', 'http://twitter.com/statuses/user_timeline.json');
+define('AKTT_API_STATUS_SHOW', 'http://twitter.com/statuses/show/###ID###.json');
+define('AKTT_PROFILE_URL', 'http://twitter.com/###USERNAME###');
+define('AKTT_STATUS_URL', 'http://twitter.com/###USERNAME###/statuses/###STATUS###');
+define('AKTT_HASHTAG_URL', 'http://search.twitter.com/search?q=###HASHTAG###');
+
+function aktt_install() {
+	global $wpdb;
+
+	$aktt_install = new twitter_tools;
+	$wpdb->aktt = $wpdb->prefix.'ak_twitter';
+	$charset_collate = '';
+	if ( version_compare(mysql_get_server_info(), '4.1.0', '>=') ) {
+		if (!empty($wpdb->charset)) {
+			$charset_collate .= " DEFAULT CHARACTER SET $wpdb->charset";
+		}
+		if (!empty($wpdb->collate)) {
+			$charset_collate .= " COLLATE $wpdb->collate";
+		}
+	}
+	$result = $wpdb->query("
+		CREATE TABLE `$wpdb->aktt` (
+		`id` INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
+		`tw_id` VARCHAR( 255 ) NOT NULL ,
+		`tw_text` VARCHAR( 255 ) NOT NULL ,
+		`tw_reply_username` VARCHAR( 255 ) DEFAULT NULL ,
+		`tw_reply_tweet` VARCHAR( 255 ) DEFAULT NULL ,
+		`tw_created_at` DATETIME NOT NULL ,
+		`modified` DATETIME NOT NULL ,
+		INDEX ( `tw_id` )
+		) $charset_collate
+	");
+	foreach ($aktt_install->options as $option) {
+		add_option('aktt_'.$option, $aktt_install->$option);
+	}
+	add_option('aktt_update_hash', '');
+}
+register_activation_hook(AKTT_FILE, 'aktt_install');
+
+class twitter_tools {
+	function twitter_tools() {
+		$this->options = array(
+			'twitter_username'
+			, 'twitter_password'
+			, 'create_blog_posts'
+			, 'create_digest'
+			, 'create_digest_weekly'
+			, 'digest_daily_time'
+			, 'digest_weekly_time'
+			, 'digest_weekly_day'
+			, 'digest_title'
+			, 'digest_title_weekly'
+			, 'blog_post_author'
+			, 'blog_post_category'
+			, 'blog_post_tags'
+			, 'notify_twitter'
+			, 'sidebar_tweet_count'
+			, 'tweet_from_sidebar'
+			, 'give_tt_credit'
+			, 'exclude_reply_tweets'
+			, 'tweet_prefix'
+			, 'last_tweet_download'
+			, 'doing_tweet_download'
+			, 'doing_digest_post'
+			, 'install_date'
+			, 'js_lib'
+			, 'digest_tweet_order'
+			, 'notify_twitter_default'
+		);
+		$this->twitter_username = '';
+		$this->twitter_password = '';
+		$this->create_blog_posts = '0';
+		$this->create_digest = '0';
+		$this->create_digest_weekly = '0';
+		$this->digest_daily_time = null;
+		$this->digest_weekly_time = null;
+		$this->digest_weekly_day = null;
+		$this->digest_title = __("Twitter Updates for %s", 'twitter-tools');
+		$this->digest_title_weekly = __("Twitter Weekly Updates for %s", 'twitter-tools');
+		$this->blog_post_author = '1';
+		$this->blog_post_category = '1';
+		$this->blog_post_tags = '';
+		$this->notify_twitter = '0';
+		$this->notify_twitter_default = '0';
+		$this->sidebar_tweet_count = '3';
+		$this->tweet_from_sidebar = '1';
+		$this->give_tt_credit = '1';
+		$this->exclude_reply_tweets = '0';
+		$this->install_date = '';
+		$this->js_lib = 'jquery';
+		$this->digest_tweet_order = 'ASC';
+		$this->tweet_prefix = 'New blog post';
+		// not included in options
+		$this->update_hash = '';
+		$this->tweet_format = $this->tweet_prefix.': %s %s';
+		$this->last_digest_post = '';
+		$this->last_tweet_download = '';
+		$this->doing_tweet_download = '0';
+		$this->doing_digest_post = '0';
+		$this->version = '1.6';
+	}
+	
+	function upgrade() {
+		global $wpdb;
+		$wpdb->aktt = $wpdb->prefix.'ak_twitter';
+
+		$col_data = $wpdb->get_results("
+			SHOW COLUMNS FROM $wpdb->aktt
+		");
+		$cols = array();
+		foreach ($col_data as $col) {
+			$cols[] = $col->Field;
+		}
+		// 1.2 schema upgrade
+		if (!in_array('tw_reply_username', $cols)) {
+			$wpdb->query("
+				ALTER TABLE `$wpdb->aktt`
+				ADD `tw_reply_username` VARCHAR( 255 ) DEFAULT NULL
+				AFTER `tw_text`
+			");
+		}
+		if (!in_array('tw_reply_tweet', $cols)) {
+			$wpdb->query("
+				ALTER TABLE `$wpdb->aktt`
+				ADD `tw_reply_tweet` VARCHAR( 255 ) DEFAULT NULL
+				AFTER `tw_reply_username`
+			");
+		}
+		$this->upgrade_default_tweet_prefix();
+	}
+	
+	function upgrade_default_tweet_prefix() {
+		$prefix = get_option('aktt_tweet_prefix');
+		if (empty($prefix)) {
+			$aktt_defaults = new twitter_tools;
+			update_option('aktt_tweet_prefix', $aktt_defaults->tweet_prefix);
+		}
+	}
+
+	function get_settings() {
+		foreach ($this->options as $option) {
+			$value = get_option('aktt_'.$option);
+			if ($option != 'tweet_prefix' || !empty($value)) {
+				$this->$option = $value;
+			}
+		}
+		$this->tweet_format = $this->tweet_prefix.': %s %s';
+	}
+	
+	// puts post fields into object propps
+	function populate_settings() {
+		foreach ($this->options as $option) {
+			$value = stripslashes($_POST['aktt_'.$option]);
+			if (isset($_POST['aktt_'.$option]) && ($option != 'tweet_prefix' || !empty($value))) {
+				$this->$option = $value;
+			}
+		}
+	}
+	
+	// puts object props into wp option storage
+	function update_settings() {
+		if (current_user_can('manage_options')) {
+			$this->sidebar_tweet_count = intval($this->sidebar_tweet_count);
+			if ($this->sidebar_tweet_count == 0) {
+				$this->sidebar_tweet_count = '3';
+			}
+			foreach ($this->options as $option) {
+				update_option('aktt_'.$option, $this->$option);
+			}
+			if (empty($this->install_date)) {
+				update_option('aktt_install_date', current_time('mysql'));
+			}
+			$this->initiate_digests();
+			$this->upgrade();
+			$this->upgrade_default_tweet_prefix();
+		}
+	}
+	
+	// figure out when the next weekly and daily digests will be
+	function initiate_digests() {
+		$next = ($this->create_digest) ? $this->calculate_next_daily_digest() : null;
+		$this->next_daily_digest = $next;
+		update_option('aktt_next_daily_digest', $next);
+		
+		$next = ($this->create_digest_weekly) ? $this->calculate_next_weekly_digest() : null;
+		$this->next_weekly_digest = $next;
+		update_option('aktt_next_weekly_digest', $next);
+	}
+	
+	function calculate_next_daily_digest() {
+		$optionDate = strtotime($this->digest_daily_time);
+		$hour_offset = date("G", $optionDate);
+		$minute_offset = date("i", $optionDate);
+		$next = mktime($hour_offset, $minute_offset, 0);
+		
+		// may have to move to next day
+		$now = time();
+		while($next < $now) {
+			$next += 60 * 60 * 24;
+		}
+		return $next;
+	}
+	
+	function calculate_next_weekly_digest() {
+		$optionDate = strtotime($this->digest_weekly_time);
+		$hour_offset = date("G", $optionDate);
+		$minute_offset = date("i", $optionDate);
+		
+		$current_day_of_month = date("j");
+		$current_day_of_week = date("w");
+		$current_month = date("n");
+		
+		// if this week's day is less than today, go for next week
+		$nextDay = $current_day_of_month - $current_day_of_week + $this->digest_weekly_day;
+		$next = mktime($hour_offset, $minute_offset, 0, $current_month, $nextDay);
+		if ($this->digest_weekly_day <= $current_day_of_week) {
+			$next = strtotime('+1 week', $next);
+		}
+		return $next;
+	}
+	
+	function ping_digests() {
+		// still busy
+		if (get_option('aktt_doing_digest_post') == '1') {
+			return;
+		}
+		// check all the digest schedules
+		if ($this->create_digest == 1) {
+			$this->ping_digest('aktt_next_daily_digest', 'aktt_last_digest_post', $this->digest_title, 60 * 60 * 24 * 1);
+		}
+		if ($this->create_digest_weekly == 1) {
+			$this->ping_digest('aktt_next_weekly_digest', 'aktt_last_digest_post_weekly', $this->digest_title_weekly, 60 * 60 * 24 * 7);
+		}
+		return;
+	}
+	
+	function ping_digest($nextDateField, $lastDateField, $title, $defaultDuration) {
+
+		$next = get_option($nextDateField);
+		
+		if ($next) {		
+			$next = $this->validateDate($next);
+			$rightNow = time();
+			if ($rightNow >= $next) {
+				$start = get_option($lastDateField);
+				$start = $this->validateDate($start, $rightNow - $defaultDuration);
+				if ($this->do_digest_post($start, $next, $title)) {
+					update_option($lastDateField, $rightNow);
+					update_option($nextDateField, $next + $defaultDuration);
+				} else {
+					update_option($lastDateField, null);
+				}
+			}
+		}
+	}
+	
+	function validateDate($in, $default = 0) {
+		if (!is_numeric($in)) {
+			// try to convert what they gave us into a date
+			$out = strtotime($in);
+			// if that doesn't work, return the default
+			if (!is_numeric($out)) {
+				return $default;
+			}
+			return $out;	
+		}
+		return $in;
+	}
+
+	function do_digest_post($start, $end, $title) {
+		
+		if (!$start || !$end) return false;
+
+		// flag us as busy
+		update_option('aktt_doing_digest_post', '1');
+		remove_action('publish_post', 'aktt_notify_twitter', 99);
+		remove_action('publish_post', 'aktt_store_post_options', 1, 2);
+		remove_action('save_post', 'aktt_store_post_options', 1, 2);
+		// see if there's any tweets in the time range
+		global $wpdb;
+		
+		$startGMT = gmdate("Y-m-d H:i:s", $start);
+		$endGMT = gmdate("Y-m-d H:i:s", $end);
+		
+		// build sql
+		$conditions = array();
+		$conditions[] = "tw_created_at >= '{$startGMT}'";
+		$conditions[] = "tw_created_at <= '{$endGMT}'";
+		$conditions[] = "tw_text NOT LIKE '$this->tweet_prefix%'";
+		if ($this->exclude_reply_tweets) {
+			$conditions[] = "tw_text NOT LIKE '@%'";
+		}
+		$where = implode(' AND ', $conditions);
+		
+		$sql = "
+			SELECT * FROM {$wpdb->aktt}
+			WHERE {$where}
+			GROUP BY tw_id
+			ORDER BY tw_created_at {$this->digest_tweet_order}
+		";
+
+		$tweets = $wpdb->get_results($sql);
+
+		if (count($tweets) > 0) {
+		
+			$tweets_to_post = array();
+			foreach ($tweets as $data) {
+				$tweet = new aktt_tweet;
+				$tweet->tw_text = $data->tw_text;
+				$tweet->tw_reply_tweet = $data->tw_reply_tweet;
+				if (!$tweet->tweet_is_post_notification() || ($tweet->tweet_is_reply() && $this->exclude_reply_tweets)) {
+					$tweets_to_post[] = $data;
+				}
+			}
+			
+			$tweets_to_post = apply_filters('aktt_tweets_to_digest_post', $tweets_to_post); // here's your chance to alter the tweet list that will be posted as the digest
+
+			if (count($tweets_to_post) > 0) {
+				$content = '<ul class="aktt_tweet_digest">'."\n";
+				foreach ($tweets_to_post as $tweet) {
+					$content .= '	<li>'.aktt_tweet_display($tweet, 'absolute').'</li>'."\n";
+				}
+				$content .= '</ul>'."\n";
+				if ($this->give_tt_credit == '1') {
+					$content .= '<p class="aktt_credit">'.__('Powered by <a href="http://alexking.org/projects/wordpress">Twitter Tools</a>', 'twitter-tools').'</p>';
+				}
+				$post_data = array(
+					'post_content' => $wpdb->escape($content),
+					'post_title' => $wpdb->escape(sprintf($title, date('Y-m-d'))),
+					'post_date' => date('Y-m-d H:i:s', $end),
+					'post_category' => array($this->blog_post_category),
+					'post_status' => 'publish',
+					'post_author' => $wpdb->escape($this->blog_post_author)
+				);
+				$post_data = apply_filters('aktt_digest_post_data', $post_data); // last chance to alter the digest content
+
+				$post_id = wp_insert_post($post_data);
+
+				add_post_meta($post_id, 'aktt_tweeted', '1', true);
+				wp_set_post_tags($post_id, $this->blog_post_tags);
+			}
+
+		}
+		add_action('publish_post', 'aktt_notify_twitter', 99);
+		add_action('publish_post', 'aktt_store_post_options', 1, 2);
+		add_action('save_post', 'aktt_store_post_options', 1, 2);
+		update_option('aktt_doing_digest_post', '0');
+		return true;
+	}
+	
+	function tweet_download_interval() {
+		return 600;
+	}
+	
+	function do_tweet($tweet = '') {
+		if (empty($this->twitter_username) 
+			|| empty($this->twitter_password) 
+			|| empty($tweet)
+			|| empty($tweet->tw_text)
+		) {
+			return;
+		}
+		$tweet = apply_filters('aktt_do_tweet', $tweet); // return false here to not tweet
+		if (!$tweet) {
+			return;
+		}
+		require_once(ABSPATH.WPINC.'/class-snoopy.php');
+		$snoop = new Snoopy;
+		$snoop->agent = 'Twitter Tools http://alexking.org/projects/wordpress';
+		$snoop->rawheaders = array(
+			'X-Twitter-Client' => 'Twitter Tools'
+			, 'X-Twitter-Client-Version' => $this->version
+			, 'X-Twitter-Client-URL' => 'http://alexking.org/projects/wordpress/twitter-tools.xml'
+		);
+		$snoop->user = $this->twitter_username;
+		$snoop->pass = $this->twitter_password;
+		$snoop->submit(
+			AKTT_API_POST_STATUS
+			, array(
+				'status' => $tweet->tw_text
+				, 'source' => 'twittertools'
+			)
+		);
+		if (strpos($snoop->response_code, '200')) {
+			update_option('aktt_last_tweet_download', strtotime('-28 minutes'));
+			return true;
+		}
+		return false;
+	}
+	
+	function do_blog_post_tweet($post_id = 0) {
+// this is only called on the publish_post hook
+		if ($this->notify_twitter == '0'
+			|| $post_id == 0
+			|| get_post_meta($post_id, 'aktt_tweeted', true) == '1'
+			|| get_post_meta($post_id, 'aktt_notify_twitter', true) == 'no'
+		) {
+			return;
+		}
+		$post = get_post($post_id);
+		// check for an edited post before TT was installed
+		if ($post->post_date <= $this->install_date) {
+			return;
+		}
+		// check for private posts
+		if ($post->post_status == 'private') {
+			return;
+		}
+		$tweet = new aktt_tweet;
+		$url = apply_filters('tweet_blog_post_url', get_permalink($post_id));
+		$tweet->tw_text = sprintf(__($this->tweet_format, 'twitter-tools'), @html_entity_decode($post->post_title, ENT_COMPAT, 'UTF-8'), $url);
+		$tweet = apply_filters('aktt_do_blog_post_tweet', $tweet, $post); // return false here to not tweet
+		if (!$tweet) {
+			return;
+		}
+		$this->do_tweet($tweet);
+		add_post_meta($post_id, 'aktt_tweeted', '1', true);
+	}
+	
+	function do_tweet_post($tweet) {
+		global $wpdb;
+		remove_action('publish_post', 'aktt_notify_twitter', 99);
+		$data = array(
+			'post_content' => $wpdb->escape(aktt_make_clickable($tweet->tw_text))
+			, 'post_title' => $wpdb->escape(trim_add_elipsis($tweet->tw_text, 30))
+			, 'post_date' => get_date_from_gmt(date('Y-m-d H:i:s', $tweet->tw_created_at))
+			, 'post_category' => array($this->blog_post_category)
+			, 'post_status' => 'publish'
+			, 'post_author' => $wpdb->escape($this->blog_post_author)
+		);
+		$data = apply_filters('aktt_do_tweet_post', $data, $tweet); // return false here to not make a blog post
+		if (!$data) {
+			return;
+		}
+		$post_id = wp_insert_post($data);
+		add_post_meta($post_id, 'aktt_twitter_id', $tweet->tw_id, true);
+		wp_set_post_tags($post_id, $this->blog_post_tags);
+		add_action('publish_post', 'aktt_notify_twitter', 99);
+	}
+}
+
+class aktt_tweet {
+	function aktt_tweet(
+		$tw_id = ''
+		, $tw_text = ''
+		, $tw_created_at = ''
+		, $tw_reply_username = null
+		, $tw_reply_tweet = null
+	) {
+		$this->id = '';
+		$this->modified = '';
+		$this->tw_created_at = $tw_created_at;
+		$this->tw_text = $tw_text;
+		$this->tw_reply_username = $tw_reply_username;
+		$this->tw_reply_tweet = $tw_reply_tweet;
+		$this->tw_id = $tw_id;
+	}
+	
+	function twdate_to_time($date) {
+		$parts = explode(' ', $date);
+		$date = strtotime($parts[1].' '.$parts[2].', '.$parts[5].' '.$parts[3]);
+		return $date;
+	}
+	
+	function tweet_post_exists() {
+		global $wpdb;
+		$test = $wpdb->get_results("
+			SELECT *
+			FROM $wpdb->postmeta
+			WHERE meta_key = 'aktt_twitter_id'
+			AND meta_value = '".$wpdb->escape($this->tw_id)."'
+		");
+		if (count($test) > 0) {
+			return true;
+		}
+		return false;
+	}
+	
+	function tweet_is_post_notification() {
+		global $aktt;
+		if (substr($this->tw_text, 0, strlen($aktt->tweet_prefix)) == $aktt->tweet_prefix) {
+			return true;
+		}
+		return false;
+	}
+	
+	function tweet_is_reply() {
+// Twitter data changed - users still expect anything starting with @ is a reply
+//		return !empty($this->tw_reply_tweet);
+		return (substr($this->tw_text, 0, 1) == '@');
+	}
+	
+	function add() {
+		global $wpdb, $aktt;
+		$wpdb->query("
+			INSERT
+			INTO $wpdb->aktt
+			( tw_id
+			, tw_text
+			, tw_reply_username
+			, tw_reply_tweet
+			, tw_created_at
+			, modified
+			)
+			VALUES
+			( '".$wpdb->escape($this->tw_id)."'
+			, '".$wpdb->escape($this->tw_text)."'
+			, '".$wpdb->escape($this->tw_reply_username)."'
+			, '".$wpdb->escape($this->tw_reply_tweet)."'
+			, '".date('Y-m-d H:i:s', $this->tw_created_at)."'
+			, NOW()
+			)
+		");
+		do_action('aktt_add_tweet', $this);
+		if ($aktt->create_blog_posts == '1' && !$this->tweet_post_exists() && !$this->tweet_is_post_notification() && (!$aktt->exclude_reply_tweets || !$this->tweet_is_reply())) {
+			$aktt->do_tweet_post($this);
+		}
+	}
+}
+
+function aktt_api_status_show_url($id) {
+	return str_replace('###ID###', $id, AKTT_API_STATUS_SHOW);
+}
+
+function aktt_profile_url($username) {
+	return str_replace('###USERNAME###', $username, AKTT_PROFILE_URL);
+}
+
+function aktt_profile_link($username, $prefix = '', $suffix = '') {
+	return $prefix.'<a href="'.aktt_profile_url($username).'" class="aktt_username">'.$username.'</a>'.$suffix;
+}
+
+function aktt_hashtag_url($hashtag) {
+	$hashtag = urlencode('#'.$hashtag);
+	return str_replace('###HASHTAG###', $hashtag, AKTT_HASHTAG_URL);
+}
+
+function aktt_hashtag_link($hashtag, $prefix = '', $suffix = '') {
+	return $prefix.'<a href="'.aktt_hashtag_url($hashtag).'" class="aktt_hashtag">'.htmlspecialchars($hashtag).'</a>'.$suffix;
+}
+
+function aktt_status_url($username, $status) {
+	return str_replace(
+		array(
+			'###USERNAME###'
+			, '###STATUS###'
+		)
+		, array(
+			$username
+			, $status
+		)
+		, AKTT_STATUS_URL
+	);
+}
+
+function aktt_login_test($username, $password) {
+	require_once(ABSPATH.WPINC.'/class-snoopy.php');
+	$snoop = new Snoopy;
+	$snoop->agent = 'Twitter Tools http://alexking.org/projects/wordpress';
+	$snoop->user = $username;
+	$snoop->pass = $password;
+	$snoop->fetch(AKTT_API_USER_TIMELINE);
+	if (strpos($snoop->response_code, '200')) {
+		return __("Login succeeded, you're good to go.", 'twitter-tools');
+	} else {
+		$json = new Services_JSON();
+		$results = $json->decode($snoop->results);
+		return sprintf(__('Sorry, login failed. Error message from Twitter: %s', 'twitter-tools'), $results->error);
+	}
+}
+
+
+function aktt_ping_digests() {
+	global $aktt;
+	$aktt->ping_digests();
+}
+
+function aktt_update_tweets() {
+	global $aktt;
+	// let the last update run for 10 minutes
+	if (time() - intval(get_option('aktt_doing_tweet_download')) < $aktt->tweet_download_interval()) {
+		return;
+	}
+	// wait 10 min between downloads
+	if (time() - intval(get_option('aktt_last_tweet_download')) < $aktt->tweet_download_interval()) {
+		return;
+	}
+	update_option('aktt_doing_tweet_download', time());
+	global $wpdb, $aktt;
+	if (empty($aktt->twitter_username) || empty($aktt->twitter_password)) {
+		update_option('aktt_doing_tweet_download', '0');
+		return;
+	}
+	require_once(ABSPATH.WPINC.'/class-snoopy.php');
+	$snoop = new Snoopy;
+	$snoop->agent = 'Twitter Tools http://alexking.org/projects/wordpress';
+	$snoop->user = $aktt->twitter_username;
+	$snoop->pass = $aktt->twitter_password;
+	$snoop->fetch(AKTT_API_USER_TIMELINE);
+
+	if (!strpos($snoop->response_code, '200')) {
+		update_option('aktt_doing_tweet_download', '0');
+		return;
+	}
+
+	$data = $snoop->results;
+	// hash results to see if they're any different than the last update, if so, return
+	$hash = md5($data);
+	if ($hash == get_option('aktt_update_hash')) {
+		update_option('aktt_last_tweet_download', time());
+		update_option('aktt_doing_tweet_download', '0');
+		return;
+	}
+	$json = new Services_JSON();
+	$tweets = $json->decode($data);
+
+	if (is_array($tweets) && count($tweets) > 0) {
+		$tweet_ids = array();
+		foreach ($tweets as $tweet) {
+			$tweet_ids[] = $wpdb->escape($tweet->id);
+		}
+		$existing_ids = $wpdb->get_col("
+			SELECT tw_id
+			FROM $wpdb->aktt
+			WHERE tw_id
+			IN ('".implode("', '", $tweet_ids)."')
+		");
+		$new_tweets = array();
+		foreach ($tweets as $tw_data) {
+			if (!$existing_ids || !in_array($tw_data->id, $existing_ids)) {
+				$tweet = new aktt_tweet(
+					$tw_data->id
+					, $tw_data->text
+				);
+				$tweet->tw_created_at = $tweet->twdate_to_time($tw_data->created_at);
+				if (!empty($tw_data->in_reply_to_status_id)) {
+					$tweet->tw_reply_tweet = $tw_data->in_reply_to_status_id;
+					$url = aktt_api_status_show_url($tw_data->in_reply_to_status_id);
+					$snoop->fetch($url);
+					if (strpos($snoop->response_code, '200') !== false) {
+						$data = $snoop->results;
+						$status = $json->decode($data);
+						$tweet->tw_reply_username = $status->user->screen_name;
+					}
+				}
+				// make sure we haven't downloaded someone else's tweets - happens sometimes due to Twitter hiccups
+				if (strtolower($tw_data->user->screen_name) == strtolower($aktt->twitter_username)) {
+					$new_tweets[] = $tweet;
+					$tweet->add();
+				}
+			}
+		}
+	}
+	aktt_reset_tweet_checking($hash, time());
+}
+
+
+function aktt_reset_tweet_checking($hash = '', $time = 0) {
+	if (!current_user_can('manage_options')) {
+		return;
+	}
+	update_option('aktt_update_hash', $hash);
+	update_option('aktt_last_tweet_download', $time);
+	update_option('aktt_doing_tweet_download', '0');
+}
+
+function aktt_reset_digests() {
+	if (!current_user_can('manage_options')) {
+		return;
+	}
+	update_option('aktt_doing_digest_post', '0');
+}
+
+function aktt_notify_twitter($post_id) {
+	global $aktt;
+	$aktt->do_blog_post_tweet($post_id);
+}
+add_action('publish_post', 'aktt_notify_twitter', 99);
+
+function aktt_sidebar_tweets() {
+	global $wpdb, $aktt;
+	if ($aktt->exclude_reply_tweets) {
+		$where = "AND tw_text NOT LIKE '@%' ";
+	}
+	else {
+		$where = '';
+	}
+	$tweets = $wpdb->get_results("
+		SELECT *
+		FROM $wpdb->aktt
+		WHERE tw_text NOT LIKE '$aktt->tweet_prefix%'
+		$where
+		GROUP BY tw_id
+		ORDER BY tw_created_at DESC
+		LIMIT $aktt->sidebar_tweet_count
+	");
+	$output = '<div class="aktt_tweets">'."\n"
+		.'	<ul>'."\n";
+	if (count($tweets) > 0) {
+		foreach ($tweets as $tweet) {
+			$output .= '		<li>'.aktt_tweet_display($tweet).'</li>'."\n";
+		}
+	}
+	else {
+		$output .= '		<li>'.__('No tweets available at the moment.', 'twitter-tools').'</li>'."\n";
+	}
+	if (!empty($aktt->twitter_username)) {
+  		$output .= '		<li class="aktt_more_updates"><a href="'.aktt_profile_url($aktt->twitter_username).'">'.__('More updates...', 'twitter-tools').'</a></li>'."\n";
+	}
+	$output .= '</ul>';
+	if ($aktt->tweet_from_sidebar == '1' && !empty($aktt->twitter_username) && !empty($aktt->twitter_password)) {
+  		$output .= aktt_tweet_form('input', 'onsubmit="akttPostTweet(); return false;"');
+		  $output .= '	<p id="aktt_tweet_posted_msg">'.__('Posting tweet...', 'twitter-tools').'</p>';
+	}
+	if ($aktt->give_tt_credit == '1') {
+		$output .= '<p class="aktt_credit">'.__('Powered by <a href="http://alexking.org/projects/wordpress">Twitter Tools</a>', 'twitter-tools').'</p>';
+	}
+	$output .= '</div>';
+	print($output);
+}
+
+function aktt_latest_tweet() {
+	global $wpdb, $aktt;
+	$tweets = $wpdb->get_results("
+		SELECT *
+		FROM $wpdb->aktt
+		WHERE tw_text NOT LIKE '$aktt->tweet_prefix%'
+		GROUP BY tw_id
+		ORDER BY tw_created_at DESC
+		LIMIT 1
+	");
+	if (count($tweets) == 1) {
+		foreach ($tweets as $tweet) {
+			$output = aktt_tweet_display($tweet);
+		}
+	}
+	else {
+		$output = __('No tweets available at the moment.', 'twitter-tools');
+	}
+	print($output);
+}
+
+function aktt_tweet_display($tweet, $time = 'relative') {
+	global $aktt;
+	$output = aktt_make_clickable(wp_specialchars($tweet->tw_text));
+	if (!empty($tweet->tw_reply_username)) {
+		$output .= 	' <a href="'.aktt_status_url($tweet->tw_reply_username, $tweet->tw_reply_tweet).'" class="aktt_tweet_reply">'.sprintf(__('in reply to %s', 'twitter-tools'), $tweet->tw_reply_username).'</a>';
+	}
+	switch ($time) {
+		case 'relative':
+			$time_display = aktt_relativeTime($tweet->tw_created_at, 3);
+			break;
+		case 'absolute':
+			$time_display = '#';
+			break;
+	}
+	$output .= ' <a href="'.aktt_status_url($aktt->twitter_username, $tweet->tw_id).'" class="aktt_tweet_time">'.$time_display.'</a>';
+	$output = apply_filters('aktt_tweet_display', $output, $tweet); // allows you to alter the tweet display output
+	return $output;
+}
+
+function aktt_make_clickable($tweet) {
+	$tweet .= ' ';
+	$tweet = preg_replace_callback(
+			'/@([a-zA-Z0-9_]{1,15})([) ])/'
+			, create_function(
+				'$matches'
+				, 'return aktt_profile_link($matches[1], \'@\', $matches[2]);'
+			)
+			, $tweet
+	);
+	$tweet = preg_replace_callback(
+		'/\ #([a-zA-Z0-9_]{1,15})/'
+		, create_function(
+			'$matches'
+			, 'return aktt_hashtag_link($matches[1], \' #\', \'\');'
+		)
+		, $tweet
+	);
+	
+	if (function_exists('make_chunky')) {
+		return make_chunky($tweet);
+	}
+	else {
+		return make_clickable($tweet);
+	}
+}
+
+function aktt_tweet_form($type = 'input', $extra = '') {
+	$output = '';
+	if (current_user_can('publish_posts')) {
+		$output .= '
+<form action="'.get_bloginfo('wpurl').'/index.php" method="post" id="aktt_tweet_form" '.$extra.'>
+	<fieldset>
+		';
+		switch ($type) {
+			case 'input':
+				$output .= '
+		<p><input type="text" size="20" maxlength="140" id="aktt_tweet_text" name="aktt_tweet_text" onkeyup="akttCharCount();" /></p>
+		<input type="hidden" name="ak_action" value="aktt_post_tweet_sidebar" />
+		<script type="text/javascript">
+		//<![CDATA[
+		function akttCharCount() {
+			var count = document.getElementById("aktt_tweet_text").value.length;
+			if (count > 0) {
+				document.getElementById("aktt_char_count").innerHTML = 140 - count;
+			}
+			else {
+				document.getElementById("aktt_char_count").innerHTML = "";
+			}
+		}
+		setTimeout("akttCharCount();", 500);
+		document.getElementById("aktt_tweet_form").setAttribute("autocomplete", "off");
+		//]]>
+		</script>
+				';
+				break;
+			case 'textarea':
+				$output .= '
+		<p><textarea type="text" cols="60" rows="5" maxlength="140" id="aktt_tweet_text" name="aktt_tweet_text" onkeyup="akttCharCount();"></textarea></p>
+		<input type="hidden" name="ak_action" value="aktt_post_tweet_admin" />
+		<script type="text/javascript">
+		//<![CDATA[
+		function akttCharCount() {
+			var count = document.getElementById("aktt_tweet_text").value.length;
+			if (count > 0) {
+				document.getElementById("aktt_char_count").innerHTML = (140 - count) + "'.__(' characters remaining', 'twitter-tools').'";
+			}
+			else {
+				document.getElementById("aktt_char_count").innerHTML = "";
+			}
+		}
+		setTimeout("akttCharCount();", 500);
+		document.getElementById("aktt_tweet_form").setAttribute("autocomplete", "off");
+		//]]>
+		</script>
+				';
+				break;
+		}
+		$output .= '
+		<p>
+			<input type="submit" id="aktt_tweet_submit" name="aktt_tweet_submit" value="'.__('Post Tweet!', 'twitter-tools').'" class="button-primary" />
+			<span id="aktt_char_count"></span>
+		</p>
+		<div class="clear"></div>
+	</fieldset>
+</form>
+		';
+	}
+	return $output;
+}
+
+function aktt_widget_init() {
+	if (!function_exists('register_sidebar_widget')) {
+		return;
+	}
+	function aktt_widget($args) {
+		extract($args);
+		$options = get_option('aktt_widget');
+		$title = $options['title'];
+		if (empty($title)) {
+		}
+		echo $before_widget . $before_title . $title . $after_title;
+		aktt_sidebar_tweets();
+		echo $after_widget;
+	}
+	register_sidebar_widget(array(__('Twitter Tools', 'twitter-tools'), 'widgets'), 'aktt_widget');
+	
+	function aktt_widget_control() {
+		$options = get_option('aktt_widget');
+		if (!is_array($options)) {
+			$options = array(
+				'title' => __("What I'm Doing...", 'twitter-tools')
+			);
+		}
+		if (isset($_POST['ak_action']) && $_POST['ak_action'] == 'aktt_update_widget_options') {
+			$options['title'] = strip_tags(stripslashes($_POST['aktt_widget_title']));
+			update_option('aktt_widget', $options);
+			// reset checking so that sidebar isn't blank if this is the first time activating
+			aktt_reset_tweet_checking();
+			aktt_update_tweets();
+		}
+
+		// Be sure you format your options to be valid HTML attributes.
+		$title = htmlspecialchars($options['title'], ENT_QUOTES);
+		
+		// Here is our little form segment. Notice that we don't need a
+		// complete form. This will be embedded into the existing form.
+		print('
+			<p style="text-align:right;"><label for="aktt_widget_title">' . __('Title:') . ' <input style="width: 200px;" id="aktt_widget_title" name="aktt_widget_title" type="text" value="'.$title.'" /></label></p>
+			<p>'.__('Find additional Twitter Tools options on the <a href="options-general.php?page=twitter-tools.php">Twitter Tools Options page</a>.', 'twitter-tools').'
+			<input type="hidden" id="ak_action" name="ak_action" value="aktt_update_widget_options" />
+		');
+	}
+	register_widget_control(array(__('Twitter Tools', 'twitter-tools'), 'widgets'), 'aktt_widget_control', 300, 100);
+
+}
+add_action('widgets_init', 'aktt_widget_init');
+
+function aktt_init() {
+	global $wpdb, $aktt;
+	$aktt = new twitter_tools;
+
+	$wpdb->aktt = $wpdb->prefix.'ak_twitter';
+
+	$aktt->get_settings();
+	if (($aktt->last_tweet_download + $aktt->tweet_download_interval()) < time()) {
+		add_action('shutdown', 'aktt_update_tweets');
+		add_action('shutdown', 'aktt_ping_digests');
+	}
+	if (is_admin() || ($aktt->tweet_from_sidebar && current_user_can('publish_posts'))) {
+		switch ($aktt->js_lib) {
+			case 'jquery':
+				wp_enqueue_script('jquery');
+				break;
+			case 'prototype':
+				wp_enqueue_script('prototype');
+				break;
+		}
+	}
+	global $wp_version;
+	if (isset($wp_version) && version_compare($wp_version, '2.5', '>=') && empty ($aktt->install_date)) {
+		add_action('admin_notices', create_function( '', "echo '<div class=\"error\"><p>".sprintf(__('Please update your <a href="%s">Twitter Tools settings</a>', 'twitter-tools'), get_bloginfo('wpurl')."/wp-admin/options-general.php?page=twitter-tools.php")."</p></div>';" ) );
+	}
+	if (!get_option('aktt_tweet_prefix')) {
+		update_option('aktt_tweet_prefix', $aktt->tweet_prefix);
+		add_action('admin_notices', create_function( '', "echo '<div class=\"error\"><p>".sprintf(__('Please update your <a href="%s">Twitter Tools settings</a>', 'twitter-tools'), get_bloginfo('wpurl')."/wp-admin/options-general.php?page=twitter-tools.php")."</p></div>';" ) );
+	}
+}
+add_action('init', 'aktt_init');
+
+function aktt_head() {
+	global $aktt;
+	if ($aktt->tweet_from_sidebar) {
+		print('
+			<link rel="stylesheet" type="text/css" href="'.get_bloginfo('wpurl').'/index.php?ak_action=aktt_css" />
+			<script type="text/javascript" src="'.get_bloginfo('wpurl').'/index.php?ak_action=aktt_js"></script>
+		');
+	}
+}
+add_action('wp_head', 'aktt_head');
+
+function aktt_head_admin() {
+	print('
+		<link rel="stylesheet" type="text/css" href="'.get_bloginfo('wpurl').'/index.php?ak_action=aktt_css_admin" />
+		<script type="text/javascript" src="'.get_bloginfo('wpurl').'/index.php?ak_action=aktt_js_admin"></script>
+	');
+}
+add_action('admin_head', 'aktt_head_admin');
+
+function aktt_request_handler() {
+	global $wpdb, $aktt;
+	if (!empty($_GET['ak_action'])) {
+		switch($_GET['ak_action']) {
+			case 'aktt_update_tweets':
+				aktt_update_tweets();
+				wp_redirect(get_bloginfo('wpurl').'/wp-admin/options-general.php?page=twitter-tools.php&tweets-updated=true');
+				die();
+				break;
+			case 'aktt_reset_tweet_checking':
+				aktt_reset_tweet_checking();
+				wp_redirect(get_bloginfo('wpurl').'/wp-admin/options-general.php?page=twitter-tools.php&tweet-checking-reset=true');
+				die();
+				break;
+			case 'aktt_reset_tweet_checking':
+				aktt_reset_digests();
+				wp_redirect(get_bloginfo('wpurl').'/wp-admin/options-general.php?page=twitter-tools.php&digest-reset=true');
+				die();
+				break;
+			case 'aktt_js':
+				remove_action('shutdown', 'aktt_ping_digests');
+				header("Content-type: text/javascript");
+				switch ($aktt->js_lib) {
+					case 'jquery':
+?>
+function akttPostTweet() {
+	var tweet_field = jQuery('#aktt_tweet_text');
+	var tweet_text = tweet_field.val();
+	if (tweet_text == '') {
+		return;
+	}
+	var tweet_msg = jQuery("#aktt_tweet_posted_msg");
+	jQuery.post(
+		"<?php bloginfo('wpurl'); ?>/index.php"
+		, {
+			ak_action: "aktt_post_tweet_sidebar"
+			, aktt_tweet_text: tweet_text
+		}
+		, function(data) {
+			tweet_msg.html(data);
+			akttSetReset();
+		}
+	);
+	tweet_field.val('').focus();
+	jQuery('#aktt_char_count').html('');
+	jQuery("#aktt_tweet_posted_msg").show();
+}
+function akttSetReset() {
+	setTimeout('akttReset();', 2000);
+}
+function akttReset() {
+	jQuery('#aktt_tweet_posted_msg').hide();
+}
+<?php
+						break;
+					case 'prototype':
+?>
+function akttPostTweet() {
+	var tweet_field = $('aktt_tweet_text');
+	var tweet_text = tweet_field.value;
+	if (tweet_text == '') {
+		return;
+	}
+	var tweet_msg = $("aktt_tweet_posted_msg");
+	var akttAjax = new Ajax.Updater(
+		tweet_msg,
+		"<?php bloginfo('wpurl'); ?>/index.php",
+		{
+			method: "post",
+			parameters: "ak_action=aktt_post_tweet_sidebar&aktt_tweet_text=" + tweet_text,
+			onComplete: akttSetReset
+		}
+	);
+	tweet_field.value = '';
+	tweet_field.focus();
+	$('aktt_char_count').innerHTML = '';
+	tweet_msg.style.display = 'block';
+}
+function akttSetReset() {
+	setTimeout('akttReset();', 2000);
+}
+function akttReset() {
+	$('aktt_tweet_posted_msg').style.display = 'none';
+}
+<?php
+						break;
+				}
+				die();
+				break;
+			case 'aktt_css':
+				remove_action('shutdown', 'aktt_ping_digests');
+				header("Content-Type: text/css");
+?>
+#aktt_tweet_form {
+	margin: 0;
+	padding: 5px 0;
+}
+#aktt_tweet_form fieldset {
+	border: 0;
+}
+#aktt_tweet_form fieldset #aktt_tweet_submit {
+	float: right;
+	margin-right: 10px;
+}
+#aktt_tweet_form fieldset #aktt_char_count {
+	color: #666;
+}
+#aktt_tweet_posted_msg {
+	background: #ffc;
+	display: none;
+	margin: 0 0 5px 0;
+	padding: 5px;
+}
+#aktt_tweet_form div.clear {
+	clear: both;
+	float: none;
+}
+<?php
+				die();
+				break;
+			case 'aktt_js_admin':
+				remove_action('shutdown', 'aktt_ping_digests');
+				header("Content-Type: text/javascript");
+				switch ($aktt->js_lib) {
+					case 'jquery':
+?>
+function akttTestLogin() {
+	var result = jQuery('#aktt_login_test_result');
+	result.show().addClass('aktt_login_result_wait').html('<?php _e('Testing...', 'twitter-tools'); ?>');
+	jQuery.post(
+		"<?php bloginfo('wpurl'); ?>/index.php"
+		, {
+			ak_action: "aktt_login_test"
+			, aktt_twitter_username: jQuery('#aktt_twitter_username').val()
+			, aktt_twitter_password: jQuery('#aktt_twitter_password').val()
+		}
+		, function(data) {
+			result.html(data).removeClass('aktt_login_result_wait');
+			setTimeout('akttTestLoginResult();', 5000);
+		}
+	);
+};
+
+function akttTestLoginResult() {
+	jQuery('#aktt_login_test_result').fadeOut('slow');
+};
+
+(function($){
+
+	jQuery.fn.timepicker = function(){
+	
+		var hrs = new Array();
+		for(var h = 1; h <= 12; hrs.push(h++));
+
+		var mins = new Array();
+		for(var m = 0; m < 60; mins.push(m++));
+
+		var ap = new Array('am', 'pm');
+
+		function pad(n) {
+			n = n.toString();
+			return n.length == 1 ? '0' + n : n;
+		}
+	
+		this.each(function() {
+
+			var v = $(this).val();
+			if (!v) v = new Date();
+
+			var d = new Date(v);
+			var h = d.getHours();
+			var m = d.getMinutes();
+			var p = (h >= 12) ? "pm" : "am";
+			h = (h > 12) ? h - 12 : h;
+
+			var output = '';
+
+			output += '<select id="h_' + this.id + '" class="timepicker">';				
+			for (var hr in hrs){
+				output += '<option value="' + pad(hrs[hr]) + '"';
+				if(parseInt(hrs[hr], 10) == h || (parseInt(hrs[hr], 10) == 12 && h == 0)) output += ' selected';
+				output += '>' + pad(hrs[hr]) + '</option>';
+			}
+			output += '</select>';
+	
+			output += '<select id="m_' + this.id + '" class="timepicker">';				
+			for (var mn in mins){
+				output += '<option value="' + pad(mins[mn]) + '"';
+				if(parseInt(mins[mn], 10) == m) output += ' selected';
+				output += '>' + pad(mins[mn]) + '</option>';
+			}
+			output += '</select>';				
+	
+			output += '<select id="p_' + this.id + '" class="timepicker">';				
+			for(var pp in ap){
+				output += '<option value="' + ap[pp] + '"';
+				if(ap[pp] == p) output += ' selected';
+				output += '>' + ap[pp] + '</option>';
+			}
+			output += '</select>';
+			
+			$(this).after(output);
+			
+			var field = this;
+			$(this).siblings('select.timepicker').change(function() {
+				var h = parseInt($('#h_' + field.id).val(), 10);
+				var m = parseInt($('#m_' + field.id).val(), 10);
+				var p = $('#p_' + field.id).val();
+	
+				if (p == "am") {
+					if (h == 12) {
+						h = 0;
+					}
+				} else if (p == "pm") {
+					if (h < 12) {
+						h += 12;
+					}
+				}
+				
+				var d = new Date();
+				d.setHours(h);
+				d.setMinutes(m);
+				
+				$(field).val(d.toUTCString());
+			}).change();
+
+		});
+
+		return this;
+	};
+	
+	jQuery.fn.daypicker = function() {
+		
+		var days = new Array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday');
+		
+		this.each(function() {
+			
+			var v = $(this).val();
+			if (!v) v = 0;
+			v = parseInt(v, 10);
+			
+			var output = "";
+			output += '<select id="d_' + this.id + '" class="daypicker">';				
+			for (var i = 0; i < days.length; i++) {
+				output += '<option value="' + i + '"';
+				if (v == i) output += ' selected';
+				output += '>' + days[i] + '</option>';
+			}
+			output += '</select>';
+			
+			$(this).after(output);
+			
+			var field = this;
+			$(this).siblings('select.daypicker').change(function() {
+				$(field).val( $(this).val() );
+			}).change();
+		
+		});
+		
+	};
+	
+	jQuery.fn.forceToggleClass = function(classNames, bOn) {
+		return this.each(function() {
+			jQuery(this)[ bOn ? "addClass" : "removeClass" ](classNames);
+		});
+	};
+	
+})(jQuery);
+
+jQuery(function() {
+
+	// add in the time and day selects
+	jQuery('form#ak_twittertools input.time').timepicker();
+	jQuery('form#ak_twittertools input.day').daypicker();
+	
+	// togglers
+	jQuery('.time_toggle .toggler').change(function() {
+		var theSelect = jQuery(this);
+		theSelect.parent('.time_toggle').forceToggleClass('active', theSelect.val() === "1");
+	}).change();
+	
+});
+<?php
+						break;
+					case 'prototype':
+?>
+function akttTestLogin() {
+	var username = $('aktt_twitter_username').value;
+	var password = $('aktt_twitter_password').value;
+	var result = $('aktt_login_test_result');
+	result.className = 'aktt_login_result_wait';
+	result.innerHTML = '<?php _e('Testing...', 'twitter-tools'); ?>';
+	var akttAjax = new Ajax.Updater(
+		result,
+		"<?php bloginfo('wpurl'); ?>/index.php",
+		{
+			method: "post",
+			parameters: "ak_action=aktt_login_test&aktt_twitter_username=" + username + "&aktt_twitter_password=" + password,
+			onComplete: akttTestLoginResult
+		}
+	);
+}
+function akttTestLoginResult() {
+	$('aktt_login_test_result').className = 'aktt_login_result';
+	Fat.fade_element('aktt_login_test_result');
+}
+<?php
+						break;
+				}
+				die();
+				break;
+			case 'aktt_css_admin':
+				remove_action('shutdown', 'aktt_ping_digests');
+				header("Content-Type: text/css");
+?>
+#aktt_tweet_form {
+	margin: 0;
+	padding: 5px 0;
+}
+#aktt_tweet_form fieldset {
+	border: 0;
+}
+#aktt_tweet_form fieldset textarea {
+	width: 95%;
+}
+#aktt_tweet_form fieldset #aktt_tweet_submit {
+	float: right;
+	margin-right: 50px;
+}
+#aktt_tweet_form fieldset #aktt_char_count {
+	color: #666;
+}
+#ak_readme {
+	height: 300px;
+	width: 95%;
+}
+#ak_twittertools .options {
+	overflow: hidden;
+	border: none;
+}
+#ak_twittertools .option {
+	overflow: hidden;
+	padding-bottom: 9px;
+	padding-top: 9px;
+}
+#ak_twittertools .option label {
+	display: block;
+	float: left;
+	width: 200px;
+	margin-right: 24px;
+	text-align: right;
+}
+#ak_twittertools .option span {
+	display: block;
+	float: left;
+	margin-left: 230px;
+	margin-top: 6px;
+	clear: left;
+}
+#ak_twittertools select,
+#ak_twittertools input {
+	float: left;
+	display: block;
+	margin-right: 6px;
+}
+#ak_twittertools p.submit {
+	overflow: hidden;
+}
+#ak_twittertools .option span {
+	color: #666;
+	display: block;
+}
+#ak_twittertools #aktt_login_test_result {
+	display: inline;
+	padding: 3px;
+}
+#ak_twittertools fieldset.options .option span.aktt_login_result_wait {
+	background: #ffc;
+}
+#ak_twittertools fieldset.options .option span.aktt_login_result {
+	background: #CFEBF7;
+	color: #000;
+}
+#ak_twittertools .timepicker,
+#ak_twittertools .daypicker {
+	display: none;
+}
+#ak_twittertools .active .timepicker,
+#ak_twittertools .active .daypicker {
+	display: block
+}
+<?php
+				die();
+				break;
+		}
+	}
+	if (!empty($_POST['ak_action'])) {
+		switch($_POST['ak_action']) {
+			case 'aktt_update_settings':
+				$aktt->populate_settings();
+				$aktt->update_settings();
+				wp_redirect(get_bloginfo('wpurl').'/wp-admin/options-general.php?page=twitter-tools.php&updated=true');
+				die();
+				break;
+			case 'aktt_post_tweet_sidebar':
+				if (!empty($_POST['aktt_tweet_text']) && current_user_can('publish_posts')) {
+					$tweet = new aktt_tweet();
+					$tweet->tw_text = stripslashes($_POST['aktt_tweet_text']);
+					if ($aktt->do_tweet($tweet)) {
+						die(__('Tweet posted.', 'twitter-tools'));
+					}
+					else {
+						die(__('Tweet post failed.', 'twitter-tools'));
+					}
+				}
+				break;
+			case 'aktt_post_tweet_admin':
+				if (!empty($_POST['aktt_tweet_text']) && current_user_can('publish_posts')) {
+					$tweet = new aktt_tweet();
+					$tweet->tw_text = stripslashes($_POST['aktt_tweet_text']);
+					if ($aktt->do_tweet($tweet)) {
+						wp_redirect(get_bloginfo('wpurl').'/wp-admin/post-new.php?page=twitter-tools.php&tweet-posted=true');
+					}
+					else {
+						wp_die(__('Oops, your tweet was not posted. Please check your username and password and that Twitter is up and running happily.', 'twitter-tools'));
+					}
+					die();
+				}
+				break;
+			case 'aktt_login_test':
+				$test = @aktt_login_test(
+					@stripslashes($_POST['aktt_twitter_username'])
+					, @stripslashes($_POST['aktt_twitter_password'])
+				);
+				die(__($test, 'twitter-tools'));
+				break;
+		}
+	}
+}
+add_action('init', 'aktt_request_handler', 10);
+
+function aktt_admin_tweet_form() {
+	global $aktt;
+	if ( $_GET['tweet-posted'] ) {
+		print('
+			<div id="message" class="updated fade">
+				<p>'.__('Tweet posted.', 'twitter-tools').'</p>
+			</div>
+		');
+	}
+	print('
+		<div class="wrap" id="aktt_write_tweet">
+	');
+	if (empty($aktt->twitter_username) || empty($aktt->twitter_password)) {
+		print('
+			<p>'.__('Please enter your <a href="http://twitter.com">Twitter</a> account information in your <a href="options-general.php?page=twitter-tools.php">Twitter Tools Options</a>.', 'twitter-tools').'</p>		
+		');
+	}
+	else {
+		print('
+			<h2>'.__('Write Tweet', 'twitter-tools').'</h2>
+			<p>'.__('This will create a new \'tweet\' in <a href="http://twitter.com">Twitter</a> using the account information in your <a href="options-general.php?page=twitter-tools.php">Twitter Tools Options</a>.', 'twitter-tools').'</p>
+			'.aktt_tweet_form('textarea').'
+		');
+	}
+	print('
+		</div>
+	');
+}
+
+function aktt_options_form() {
+	global $wpdb, $aktt;
+
+	$categories = get_categories('hide_empty=0');
+	$cat_options = '';
+	foreach ($categories as $category) {
+// WP < 2.3 compatibility
+		!empty($category->term_id) ? $cat_id = $category->term_id : $cat_id = $category->cat_ID;
+		!empty($category->name) ? $cat_name = $category->name : $cat_name = $category->cat_name;
+		if ($cat_id == $aktt->blog_post_category) {
+			$selected = 'selected="selected"';
+		}
+		else {
+			$selected = '';
+		}
+		$cat_options .= "\n\t<option value='$cat_id' $selected>$cat_name</option>";
+	}
+
+	$authors = get_users_of_blog();
+	$author_options = '';
+	foreach ($authors as $user) {
+		$usero = new WP_User($user->user_id);
+		$author = $usero->data;
+		// Only list users who are allowed to publish
+		if (! $usero->has_cap('publish_posts')) {
+			continue;
+		}
+		if ($author->ID == $aktt->blog_post_author) {
+			$selected = 'selected="selected"';
+		}
+		else {
+			$selected = '';
+		}
+		$author_options .= "\n\t<option value='$author->ID' $selected>$author->user_nicename</option>";
+	}
+	
+	$js_libs = array(
+		'jquery' => 'jQuery'
+		, 'prototype' => 'Prototype'
+	);
+	$js_lib_options = '';
+	foreach ($js_libs as $js_lib => $js_lib_display) {
+		if ($js_lib == $aktt->js_lib) {
+			$selected = 'selected="selected"';
+		}
+		else {
+			$selected = '';
+		}
+		$js_lib_options .= "\n\t<option value='$js_lib' $selected>$js_lib_display</option>";
+	}
+	$digest_tweet_orders = array(
+		'ASC' => __('Oldest first (Chronological order)', 'twitter-tools'),
+		'DESC' => __('Newest first (Reverse-chronological order)', 'twitter-tools')
+	);
+	$digest_tweet_order_options = '';
+	foreach ($digest_tweet_orders as $digest_tweet_order => $digest_tweet_order_display) {
+		if ($digest_tweet_order == $aktt->digest_tweet_order) {
+			$selected = 'selected="selected"';
+		}
+		else {
+			$selected = '';
+		}
+		$digest_tweet_order_options .= "\n\t<option value='$digest_tweet_order' $selected>$digest_tweet_order_display</option>";
+	}	
+	$yes_no = array(
+		'create_blog_posts'
+		, 'create_digest'
+		, 'create_digest_weekly'
+		, 'notify_twitter'
+		, 'notify_twitter_default'
+		, 'tweet_from_sidebar'
+		, 'give_tt_credit'
+		, 'exclude_reply_tweets'
+	);
+	foreach ($yes_no as $key) {
+		$var = $key.'_options';
+		if ($aktt->$key == '0') {
+			$$var = '
+				<option value="0" selected="selected">'.__('No', 'twitter-tools').'</option>
+				<option value="1">'.__('Yes', 'twitter-tools').'</option>
+			';
+		}
+		else {
+			$$var = '
+				<option value="0">'.__('No', 'twitter-tools').'</option>
+				<option value="1" selected="selected">'.__('Yes', 'twitter-tools').'</option>
+			';
+		}
+	}
+	if ( $_GET['tweets-updated'] ) {
+		print('
+			<div id="message" class="updated fade">
+				<p>'.__('Tweets updated.', 'twitter-tools').'</p>
+			</div>
+		');
+	}
+	if ( $_GET['tweet-checking-reset'] ) {
+		print('
+			<div id="message" class="updated fade">
+				<p>'.__('Tweet checking has been reset.', 'twitter-tools').'</p>
+			</div>
+		');
+	}
+	print('
+			<div class="wrap" id="aktt_options_page">
+				<h2>'.__('Twitter Tools Options', 'twitter-tools').'</h2>
+				<form id="ak_twittertools" name="ak_twittertools" action="'.get_bloginfo('wpurl').'/wp-admin/options-general.php" method="post">
+					<fieldset class="options">
+						<div class="option">
+							<label for="aktt_twitter_username">'.__('Twitter Username', 'twitter-tools').'/'.__('Password', 'twitter-tools').'</label>
+							<input type="text" size="25" name="aktt_twitter_username" id="aktt_twitter_username" value="'.$aktt->twitter_username.'" autocomplete="off" />
+							<input type="password" size="25" name="aktt_twitter_password" id="aktt_twitter_password" value="'.$aktt->twitter_password.'" autocomplete="off" />
+							<input type="button" class="button" name="aktt_login_test" id="aktt_login_test" value="'.__('Test Login Info', 'twitter-tools').'" onclick="akttTestLogin(); return false;" />
+							<span id="aktt_login_test_result"></span>
+						</div>
+						<div class="option">
+							<label for="aktt_notify_twitter">'.__('Enable option to create a tweet when you post in your blog?', 'twitter-tools').'</label>
+							<select name="aktt_notify_twitter" id="aktt_notify_twitter">'.$notify_twitter_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_tweet_prefix">'.__('Tweet prefix for new blog posts:', 'twitter-tools').'</label>
+							<input type="text" size="30" name="aktt_tweet_prefix" id="aktt_tweet_prefix" value="'.$aktt->tweet_prefix.'" /><span>'.__('Cannot be left blank. Will result in <b>{Your prefix}: Title URL</b>', 'twitter-tools').'</span>
+						</div>
+						<div class="option">
+							<label for="aktt_notify_twitter_default">'.__('Set this on by default?', 'twitter-tools').'</label>
+							<select name="aktt_notify_twitter_default" id="aktt_notify_twitter_default">'.$notify_twitter_default_options.'</select><span>'							.__('Also determines tweeting for posting via XML-RPC', 'twitter-tools').'</span>
+						</div>
+						<div class="option">
+							<label for="aktt_create_blog_posts">'.__('Create a blog post from each of your tweets?', 'twitter-tools').'</label>
+							<select name="aktt_create_blog_posts" id="aktt_create_blog_posts">'.$create_blog_posts_options.'</select>
+						</div>
+						<div class="option time_toggle">
+							<label>'.__('Create a daily digest blog post from your tweets?', 'twitter-tools').'</label>
+							<select name="aktt_create_digest" class="toggler">'.$create_digest_options.'</select>
+							<input type="hidden" class="time" id="aktt_digest_daily_time" name="aktt_digest_daily_time" value="'.$aktt->digest_daily_time.'" />
+						</div>
+						<div class="option">
+							<label for="aktt_digest_title">'.__('Title for daily digest posts:', 'twitter-tools').'</label>
+							<input type="text" size="30" name="aktt_digest_title" id="aktt_digest_title" value="'.$aktt->digest_title.'" />
+							<span>'.__('Include %s where you want the date. Example: Tweets on %s', 'twitter-tools').'</span>
+						</div>
+						<div class="option time_toggle">
+							<label>'.__('Create a weekly digest blog post from your tweets?', 'twitter-tools').'</label>
+							<select name="aktt_create_digest_weekly" class="toggler">'.$create_digest_weekly_options.'</select>
+							<input type="hidden" class="time" name="aktt_digest_weekly_time" id="aktt_digest_weekly_time" value="'.$aktt->digest_weekly_time.'" />
+							<input type="hidden" class="day" name="aktt_digest_weekly_day" value="'.$aktt->digest_weekly_day.'" />
+						</div>
+						<div class="option">
+							<label for="aktt_digest_title_weekly">'.__('Title for weekly digest posts:', 'twitter-tools').'</label>
+							<input type="text" size="30" name="aktt_digest_title_weekly" id="aktt_digest_title_weekly" value="'.$aktt->digest_title_weekly.'" />
+							<span>'.__('Include %s where you want the date. Example: Tweets on %s', 'twitter-tools').'</span>
+						</div>
+						<div class="option">
+							<label for="aktt_digest_tweet_order">'.__('Order of tweets in digest?', 'twitter-tools').'</label>
+							<select name="aktt_digest_tweet_order" id="aktt_digest_tweet_order">'.$digest_tweet_order_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_blog_post_category">'.__('Category for tweet posts:', 'twitter-tools').'</label>
+							<select name="aktt_blog_post_category" id="aktt_blog_post_category">'.$cat_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_blog_post_tags">'.__('Tag(s) for your tweet posts:', 'twitter-tools').'</label>
+							<input name="aktt_blog_post_tags" id="aktt_blog_post_tags" value="'.$aktt->blog_post_tags.'">
+							<span>'.__('Separate multiple tags with commas. Example: tweets, twitter', 'twitter-tools').'</span>
+						</div>
+						<div class="option">
+							<label for="aktt_blog_post_author">'.__('Author for tweet posts:', 'twitter-tools').'</label>
+							<select name="aktt_blog_post_author" id="aktt_blog_post_author">'.$author_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_exclude_reply_tweets">'.__('Exclude @reply tweets in your sidebar, digests and created blog posts?', 'twitter-tools').'</label>
+							<select name="aktt_exclude_reply_tweets" id="aktt_exclude_reply_tweets">'.$exclude_reply_tweets_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_sidebar_tweet_count">'.__('Tweets to show in sidebar:', 'twitter-tools').'</label>
+							<input type="text" size="3" name="aktt_sidebar_tweet_count" id="aktt_sidebar_tweet_count" value="'.$aktt->sidebar_tweet_count.'" />
+							<span>'.__('Numbers only please.', 'twitter-tools').'</span>
+						</div>
+						<div class="option">
+							<label for="aktt_tweet_from_sidebar">'.__('Create tweets from your sidebar?', 'twitter-tools').'</label>
+							<select name="aktt_tweet_from_sidebar" id="aktt_tweet_from_sidebar">'.$tweet_from_sidebar_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_js_lib">'.__('JS Library to use?', 'twitter-tools').'</label>
+							<select name="aktt_js_lib" id="aktt_js_lib">'.$js_lib_options.'</select>
+						</div>
+						<div class="option">
+							<label for="aktt_give_tt_credit">'.__('Give Twitter Tools credit?', 'twitter-tools').'</label>
+							<select name="aktt_give_tt_credit" id="aktt_give_tt_credit">'.$give_tt_credit_options.'</select>
+						</div>
+					</fieldset>
+					<p class="submit">
+						<input type="submit" name="submit" class="button-primary" value="'.__('Update Twitter Tools Options', 'twitter-tools').'" />
+					</p>
+					<input type="hidden" name="ak_action" value="aktt_update_settings" class="hidden" style="display: none;" />
+				</form>
+				<h2>'.__('Update Tweets / Reset Checking and Digests', 'twitter-tools').'</h2>
+				<form name="ak_twittertools_updatetweets" action="'.get_bloginfo('wpurl').'/wp-admin/options-general.php" method="get">
+					<p>'.__('Use these buttons to manually update your tweets or reset the checking settings.', 'twitter-tools').'</p>
+					<p class="submit">
+						<input type="submit" name="submit-button" value="'.__('Update Tweets', 'twitter-tools').'" />
+						<input type="submit" name="reset-button-1" value="'.__('Reset Tweet Checking', 'twitter-tools').'" onclick="document.getElementById(\'ak_action_2\').value = \'aktt_reset_tweet_checking\';" />
+						<input type="submit" name="reset-button-2" value="'.__('Reset Digests', 'twitter-tools').'" onclick="document.getElementById(\'ak_action_2\').value = \'aktt_reset_digests\';" />
+						<input type="hidden" name="ak_action" id="ak_action_2" value="aktt_update_tweets" />
+					</p>
+				</form>
+	');
+	do_action('aktt_options_form');
+	print('
+				
+				<h2>'.__('README', 'twitter-tools').'</h2>
+				<p>'.__('Find answers to common questions here.', 'twitter-tools').'</p>
+				<iframe id="ak_readme" src="http://alexking.org/projects/wordpress/readme?project=twitter-tools"></iframe>
+			</div>
+	');
+}
+
+function aktt_post_options() {
+	global $aktt, $post;
+	if ($aktt->notify_twitter) {
+		echo '<div class="postbox">
+			<h3>'.__('Twitter Tools', 'twitter-tools').'</h3>
+			<div class="inside">
+			<p>'.__('Notify Twitter about this post?', 'twitter-tools');
+		$notify = get_post_meta($post->ID, 'aktt_notify_twitter', true);
+		if ($notify == '') {
+			switch ($aktt->notify_twitter_default) {
+				case '1':
+					$notify = 'yes';
+					break;
+				case '0':
+					$notify = 'no';
+					break;
+			}
+		}
+		if ($notify == 'no') {
+			$yes = '';
+			$no = 'checked="checked"';
+		}
+		else {
+			$yes = 'checked="checked"';
+			$no = '';
+		}
+		echo '
+		<input type="radio" name="aktt_notify_twitter" id="aktt_notify_twitter_yes" value="yes" '.$yes.' /> <label for="aktt_notify_twitter_yes">'.__('Yes', 'twitter-tools').'</label> &nbsp;&nbsp;
+		<input type="radio" name="aktt_notify_twitter" id="aktt_notify_twitter_no" value="no" '.$no.' /> <label for="aktt_notify_twitter_no">'.__('No', 'twitter-tools').'</label>
+		';
+		echo '
+			</p>
+		';
+		do_action('aktt_post_options');
+		echo '
+			</div><!--.inside-->
+			</div><!--.postbox-->
+		';
+	}
+}
+add_action('edit_form_advanced', 'aktt_post_options');
+
+function aktt_store_post_options($post_id, $post = false) {
+	global $aktt;
+	$post = get_post($post_id);
+	if (!$post || $post->post_type == 'revision') {
+		return;
+	}
+
+	$notify_meta = get_post_meta($post_id, 'aktt_notify_twitter', true);
+	$posted_meta = $_POST['aktt_notify_twitter'];
+
+	$save = false;
+	if (!empty($posted_meta)) {
+		$posted_meta == 'yes' ? $meta = 'yes' : $meta = 'no';
+		$save = true;
+	}
+	else if (empty($notify_meta)) {
+		$aktt->notify_twitter_default ? $meta = 'yes' : $meta = 'no';
+		$save = true;
+	}
+	else {
+		$save = false;
+	}
+	
+	if ($save) {
+		if (!update_post_meta($post_id, 'aktt_notify_twitter', $meta)) {
+			add_post_meta($post_id, 'aktt_notify_twitter', $meta);
+		}
+	}
+}
+add_action('draft_post', 'aktt_store_post_options', 1, 2);
+add_action('publish_post', 'aktt_store_post_options', 1, 2);
+add_action('save_post', 'aktt_store_post_options', 1, 2);
+
+function aktt_menu_items() {
+	if (current_user_can('manage_options')) {
+		add_options_page(
+			__('Twitter Tools Options', 'twitter-tools')
+			, __('Twitter Tools', 'twitter-tools')
+			, 10
+			, basename(__FILE__)
+			, 'aktt_options_form'
+		);
+	}
+	if (current_user_can('publish_posts')) {
+		add_submenu_page(
+			'post-new.php'
+			, __('New Tweet', 'twitter-tools')
+			, __('Tweet', 'twitter-tools')
+			, 2
+			, basename(__FILE__)
+			, 'aktt_admin_tweet_form'
+		);
+	}
+}
+add_action('admin_menu', 'aktt_menu_items');
+
+function aktt_plugin_action_links($links, $file) {
+	$plugin_file = basename(__FILE__);
+	if (basename($file) == $plugin_file) {
+		$settings_link = '<a href="options-general.php?page='.$plugin_file.'">'.__('Settings', 'twitter-tools').'</a>';
+		array_unshift($links, $settings_link);
+	}
+	return $links;
+}
+add_filter('plugin_action_links', 'aktt_plugin_action_links', 10, 2);
+
+if (!function_exists('trim_add_elipsis')) {
+	function trim_add_elipsis($string, $limit = 100) {
+		if (strlen($string) > $limit) {
+			$string = substr($string, 0, $limit)."...";
+		}
+		return $string;
+	}
+}
+
+if (!function_exists('ak_gmmktime')) {
+	function ak_gmmktime() {
+		return gmmktime() - get_option('gmt_offset') * 3600;
+	}
+}
+
+/**
+
+based on: http://www.gyford.com/phil/writing/2006/12/02/quick_twitter.php
+
+	 * Returns a relative date, eg "4 hrs ago".
+	 *
+	 * Assumes the passed-in can be parsed by strtotime.
+	 * Precision could be one of:
+	 * 	1	5 hours, 3 minutes, 2 seconds ago (not yet implemented).
+	 * 	2	5 hours, 3 minutes
+	 * 	3	5 hours
+	 *
+	 * This is all a little overkill, but copied from other places I've used it.
+	 * Also superfluous, now I've noticed that the Twitter API includes something
+	 * similar, but this version is more accurate and less verbose.
+	 *
+	 * @access private.
+	 * @param string date In a format parseable by strtotime().
+	 * @param integer precision
+	 * @return string
+	 */
+function aktt_relativeTime ($date, $precision=2)
+{
+
+	$now = time();
+
+	$time = gmmktime(
+		substr($date, 11, 2)
+		, substr($date, 14, 2)
+		, substr($date, 17, 2)
+		, substr($date, 5, 2)
+		, substr($date, 8, 2)
+		, substr($date, 0, 4)
+	);
+
+	$time = strtotime(date('Y-m-d H:i:s', $time));
+
+	$diff 	=  $now - $time;
+
+	$months	=  floor($diff/2419200);
+	$diff 	-= $months * 2419200;
+	$weeks 	=  floor($diff/604800);
+	$diff	-= $weeks*604800;
+	$days 	=  floor($diff/86400);
+	$diff 	-= $days * 86400;
+	$hours 	=  floor($diff/3600);
+	$diff 	-= $hours * 3600;
+	$minutes = floor($diff/60);
+	$diff 	-= $minutes * 60;
+	$seconds = $diff;
+
+	if ($months > 0) {
+		return date_i18n( __('Y-m-d', 'twitter-tools'), $time);
+	} else {
+		$relative_date = '';
+		if ($weeks > 0) {
+			// Weeks and days
+			$relative_date .= ($relative_date?', ':'').$weeks.' '.__ngettext('week', 'weeks', $weeks, 'twitter-tools');
+			if ($precision <= 2) {
+				$relative_date .= $days>0? ($relative_date?', ':'').$days.' '.__ngettext('day', 'days', $days, 'twitter-tools'):'';
+				if ($precision == 1) {
+					$relative_date .= $hours>0?($relative_date?', ':'').$hours.' '.__ngettext('hr', 'hrs', $hours, 'twitter-tools'):'';
+				}
+			}
+		} elseif ($days > 0) {
+			// days and hours
+			$relative_date .= ($relative_date?', ':'').$days.' '.__ngettext('day', 'days', $days, 'twitter-tools');
+			if ($precision <= 2) {
+				$relative_date .= $hours>0?($relative_date?', ':'').$hours.' '.__ngettext('hr', 'hrs', $hours, 'twitter-tools'):'';
+				if ($precision == 1) {
+					$relative_date .= $minutes>0?($relative_date?', ':'').$minutes.' '.__ngettext('min', 'mins', $minutes, 'twitter-tools'):'';
+				}
+			}
+		} elseif ($hours > 0) {
+			// hours and minutes
+			$relative_date .= ($relative_date?', ':'').$hours.' '.__ngettext('hr', 'hrs', $hours, 'twitter-tools');
+			if ($precision <= 2) {
+				$relative_date .= $minutes>0?($relative_date?', ':'').$minutes.' '.__ngettext('min', 'mins', $minutes, 'twitter-tools'):'';
+				if ($precision == 1) {
+					$relative_date .= $seconds>0?($relative_date?', ':'').$seconds.' '.__ngettext('sec', 'secs', $seconds, 'twitter-tools'):'';
+				}
+			}
+		} elseif ($minutes > 0) {
+			// minutes only
+			$relative_date .= ($relative_date?', ':'').$minutes.' '.__ngettext('min', 'mins', $minutes, 'twitter-tools');
+			if ($precision == 1) {
+				$relative_date .= $seconds>0?($relative_date?', ':'').$seconds.' '.__ngettext('sec', 'secs', $seconds, 'twitter-tools'):'';
+			}
+		} else {
+			// seconds only
+			$relative_date .= ($relative_date?', ':'').$seconds.' '.__ngettext('sec', 'secs', $seconds, 'twitter-tools');
+		}
+	}
+
+	// Return relative date and add proper verbiage
+	return sprintf(__('%s ago', 'twitter-tools'), $relative_date);
+}
+if (!class_exists('Services_JSON')) {
+
+// PEAR JSON class
+
+/**
+* Converts to and from JSON format.
+*
+* JSON (JavaScript Object Notation) is a lightweight data-interchange
+* format. It is easy for humans to read and write. It is easy for machines
+* to parse and generate. It is based on a subset of the JavaScript
+* Programming Language, Standard ECMA-262 3rd Edition - December 1999.
+* This feature can also be found in  Python. JSON is a text format that is
+* completely language independent but uses conventions that are familiar
+* to programmers of the C-family of languages, including C, C++, C#, Java,
+* JavaScript, Perl, TCL, and many others. These properties make JSON an
+* ideal data-interchange language.
+*
+* This package provides a simple encoder and decoder for JSON notation. It
+* is intended for use with client-side Javascript applications that make
+* use of HTTPRequest to perform server communication functions - data can
+* be encoded into JSON notation for use in a client-side javascript, or
+* decoded from incoming Javascript requests. JSON format is native to
+* Javascript, and can be directly eval()'ed with no further parsing
+* overhead
+*
+* All strings should be in ASCII or UTF-8 format!
+*
+* LICENSE: Redistribution and use in source and binary forms, with or
+* without modification, are permitted provided that the following
+* conditions are met: Redistributions of source code must retain the
+* above copyright notice, this list of conditions and the following
+* disclaimer. Redistributions in binary form must reproduce the above
+* copyright notice, this list of conditions and the following disclaimer
+* in the documentation and/or other materials provided with the
+* distribution.
+*
+* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+* NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+* DAMAGE.
+*
+* @category
+* @package     Services_JSON
+* @author      Michal Migurski <mike-json@teczno.com>
+* @author      Matt Knapp <mdknapp[at]gmail[dot]com>
+* @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+* @copyright   2005 Michal Migurski
+* @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
+* @license     http://www.opensource.org/licenses/bsd-license.php
+* @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
+*/
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_SLICE',   1);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_STR',  2);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_ARR',  3);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_OBJ',  4);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_CMT', 5);
+
+/**
+* Behavior switch for Services_JSON::decode()
+*/
+define('SERVICES_JSON_LOOSE_TYPE', 16);
+
+/**
+* Behavior switch for Services_JSON::decode()
+*/
+define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
+
+/**
+* Converts to and from JSON format.
+*
+* Brief example of use:
+*
+* <code>
+* // create a new instance of Services_JSON
+* $json = new Services_JSON();
+*
+* // convert a complexe value to JSON notation, and send it to the browser
+* $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
+* $output = $json->encode($value);
+*
+* print($output);
+* // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
+*
+* // accept incoming POST data, assumed to be in JSON notation
+* $input = file_get_contents('php://input', 1000000);
+* $value = $json->decode($input);
+* </code>
+*/
+class Services_JSON
+{
+   /**
+    * constructs a new JSON instance
+    *
+    * @param    int     $use    object behavior flags; combine with boolean-OR
+    *
+    *                           possible values:
+    *                           - SERVICES_JSON_LOOSE_TYPE:  loose typing.
+    *                                   "{...}" syntax creates associative arrays
+    *                                   instead of objects in decode().
+    *                           - SERVICES_JSON_SUPPRESS_ERRORS:  error suppression.
+    *                                   Values which can't be encoded (e.g. resources)
+    *                                   appear as NULL instead of throwing errors.
+    *                                   By default, a deeply-nested resource will
+    *                                   bubble up with an error, so all return values
+    *                                   from encode() should be checked with isError()
+    */
+    function Services_JSON($use = 0)
+    {
+        $this->use = $use;
+    }
+
+   /**
+    * convert a string from one UTF-16 char to one UTF-8 char
+    *
+    * Normally should be handled by mb_convert_encoding, but
+    * provides a slower PHP-only method for installations
+    * that lack the multibye string extension.
+    *
+    * @param    string  $utf16  UTF-16 character
+    * @return   string  UTF-8 character
+    * @access   private
+    */
+    function utf162utf8($utf16)
+    {
+        // oh please oh please oh please oh please oh please
+        if(function_exists('mb_convert_encoding')) {
+            return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
+        }
+
+        $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
+
+        switch(true) {
+            case ((0x7F & $bytes) == $bytes):
+                // this case should never be reached, because we are in ASCII range
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0x7F & $bytes);
+
+            case (0x07FF & $bytes) == $bytes:
+                // return a 2-byte UTF-8 character
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0xC0 | (($bytes >> 6) & 0x1F))
+                     . chr(0x80 | ($bytes & 0x3F));
+
+            case (0xFFFF & $bytes) == $bytes:
+                // return a 3-byte UTF-8 character
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0xE0 | (($bytes >> 12) & 0x0F))
+                     . chr(0x80 | (($bytes >> 6) & 0x3F))
+                     . chr(0x80 | ($bytes & 0x3F));
+        }
+
+        // ignoring UTF-32 for now, sorry
+        return '';
+    }
+
+   /**
+    * convert a string from one UTF-8 char to one UTF-16 char
+    *
+    * Normally should be handled by mb_convert_encoding, but
+    * provides a slower PHP-only method for installations
+    * that lack the multibye string extension.
+    *
+    * @param    string  $utf8   UTF-8 character
+    * @return   string  UTF-16 character
+    * @access   private
+    */
+    function utf82utf16($utf8)
+    {
+        // oh please oh please oh please oh please oh please
+        if(function_exists('mb_convert_encoding')) {
+            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+        }
+
+        switch(strlen($utf8)) {
+            case 1:
+                // this case should never be reached, because we are in ASCII range
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return $utf8;
+
+            case 2:
+                // return a UTF-16 character from a 2-byte UTF-8 char
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0x07 & (ord($utf8{0}) >> 2))
+                     . chr((0xC0 & (ord($utf8{0}) << 6))
+                         | (0x3F & ord($utf8{1})));
+
+            case 3:
+                // return a UTF-16 character from a 3-byte UTF-8 char
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr((0xF0 & (ord($utf8{0}) << 4))
+                         | (0x0F & (ord($utf8{1}) >> 2)))
+                     . chr((0xC0 & (ord($utf8{1}) << 6))
+                         | (0x7F & ord($utf8{2})));
+        }
+
+        // ignoring UTF-32 for now, sorry
+        return '';
+    }
+
+   /**
+    * encodes an arbitrary variable into JSON format
+    *
+    * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
+    *                           see argument 1 to Services_JSON() above for array-parsing behavior.
+    *                           if var is a strng, note that encode() always expects it
+    *                           to be in ASCII or UTF-8 format!
+    *
+    * @return   mixed   JSON string representation of input var or an error if a problem occurs
+    * @access   public
+    */
+    function encode($var)
+    {
+        switch (gettype($var)) {
+            case 'boolean':
+                return $var ? 'true' : 'false';
+
+            case 'NULL':
+                return 'null';
+
+            case 'integer':
+                return (int) $var;
+
+            case 'double':
+            case 'float':
+                return (float) $var;
+
+            case 'string':
+                // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+                $ascii = '';
+                $strlen_var = strlen($var);
+
+               /*
+                * Iterate over every character in the string,
+                * escaping with a slash or encoding to UTF-8 where necessary
+                */
+                for ($c = 0; $c < $strlen_var; ++$c) {
+
+                    $ord_var_c = ord($var{$c});
+
+                    switch (true) {
+                        case $ord_var_c == 0x08:
+                            $ascii .= '\b';
+                            break;
+                        case $ord_var_c == 0x09:
+                            $ascii .= '\t';
+                            break;
+                        case $ord_var_c == 0x0A:
+                            $ascii .= '\n';
+                            break;
+                        case $ord_var_c == 0x0C:
+                            $ascii .= '\f';
+                            break;
+                        case $ord_var_c == 0x0D:
+                            $ascii .= '\r';
+                            break;
+
+                        case $ord_var_c == 0x22:
+                        case $ord_var_c == 0x2F:
+                        case $ord_var_c == 0x5C:
+                            // double quote, slash, slosh
+                            $ascii .= '\\'.$var{$c};
+                            break;
+
+                        case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+                            // characters U-00000000 - U-0000007F (same as ASCII)
+                            $ascii .= $var{$c};
+                            break;
+
+                        case (($ord_var_c & 0xE0) == 0xC0):
+                            // characters U-00000080 - U-000007FF, mask 110XXXXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
+                            $c += 1;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xF0) == 0xE0):
+                            // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}));
+                            $c += 2;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xF8) == 0xF0):
+                            // characters U-00010000 - U-001FFFFF, mask 11110XXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}));
+                            $c += 3;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xFC) == 0xF8):
+                            // characters U-00200000 - U-03FFFFFF, mask 111110XX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}),
+                                         ord($var{$c + 4}));
+                            $c += 4;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xFE) == 0xFC):
+                            // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}),
+                                         ord($var{$c + 4}),
+                                         ord($var{$c + 5}));
+                            $c += 5;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+                    }
+                }
+
+                return '"'.$ascii.'"';
+
+            case 'array':
+               /*
+                * As per JSON spec if any array key is not an integer
+                * we must treat the the whole array as an object. We
+                * also try to catch a sparsely populated associative
+                * array with numeric keys here because some JS engines
+                * will create an array with empty indexes up to
+                * max_index which can cause memory issues and because
+                * the keys, which may be relevant, will be remapped
+                * otherwise.
+                *
+                * As per the ECMA and JSON specification an object may
+                * have any string as a property. Unfortunately due to
+                * a hole in the ECMA specification if the key is a
+                * ECMA reserved word or starts with a digit the
+                * parameter is only accessible using ECMAScript's
+                * bracket notation.
+                */
+
+                // treat as a JSON object
+                if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
+                    $properties = array_map(array($this, 'name_value'),
+                                            array_keys($var),
+                                            array_values($var));
+
+                    foreach($properties as $property) {
+                        if(Services_JSON::isError($property)) {
+                            return $property;
+                        }
+                    }
+
+                    return '{' . join(',', $properties) . '}';
+                }
+
+                // treat it like a regular array
+                $elements = array_map(array($this, 'encode'), $var);
+
+                foreach($elements as $element) {
+                    if(Services_JSON::isError($element)) {
+                        return $element;
+                    }
+                }
+
+                return '[' . join(',', $elements) . ']';
+
+            case 'object':
+                $vars = get_object_vars($var);
+
+                $properties = array_map(array($this, 'name_value'),
+                                        array_keys($vars),
+                                        array_values($vars));
+
+                foreach($properties as $property) {
+                    if(Services_JSON::isError($property)) {
+                        return $property;
+                    }
+                }
+
+                return '{' . join(',', $properties) . '}';
+
+            default:
+                return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
+                    ? 'null'
+                    : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
+        }
+    }
+
+   /**
+    * array-walking function for use in generating JSON-formatted name-value pairs
+    *
+    * @param    string  $name   name of key to use
+    * @param    mixed   $value  reference to an array element to be encoded
+    *
+    * @return   string  JSON-formatted name-value pair, like '"name":value'
+    * @access   private
+    */
+    function name_value($name, $value)
+    {
+        $encoded_value = $this->encode($value);
+
+        if(Services_JSON::isError($encoded_value)) {
+            return $encoded_value;
+        }
+
+        return $this->encode(strval($name)) . ':' . $encoded_value;
+    }
+
+   /**
+    * reduce a string by removing leading and trailing comments and whitespace
+    *
+    * @param    $str    string      string value to strip of comments and whitespace
+    *
+    * @return   string  string value stripped of comments and whitespace
+    * @access   private
+    */
+    function reduce_string($str)
+    {
+        $str = preg_replace(array(
+
+                // eliminate single line comments in '// ...' form
+                '#^\s*//(.+)$#m',
+
+                // eliminate multi-line comments in '/* ... */' form, at start of string
+                '#^\s*/\*(.+)\*/#Us',
+
+                // eliminate multi-line comments in '/* ... */' form, at end of string
+                '#/\*(.+)\*/\s*$#Us'
+
+            ), '', $str);
+
+        // eliminate extraneous space
+        return trim($str);
+    }
+
+   /**
+    * decodes a JSON string into appropriate variable
+    *
+    * @param    string  $str    JSON-formatted string
+    *
+    * @return   mixed   number, boolean, string, array, or object
+    *                   corresponding to given JSON input string.
+    *                   See argument 1 to Services_JSON() above for object-output behavior.
+    *                   Note that decode() always returns strings
+    *                   in ASCII or UTF-8 format!
+    * @access   public
+    */
+    function decode($str)
+    {
+        $str = $this->reduce_string($str);
+
+        switch (strtolower($str)) {
+            case 'true':
+                return true;
+
+            case 'false':
+                return false;
+
+            case 'null':
+                return null;
+
+            default:
+                $m = array();
+
+                if (is_numeric($str)) {
+                    // Lookie-loo, it's a number
+
+                    // This would work on its own, but I'm trying to be
+                    // good about returning integers where appropriate:
+                    // return (float)$str;
+
+                    // Return float or int, as appropriate
+                    return ((float)$str == (integer)$str)
+                        ? (integer)$str
+                        : (float)$str;
+
+                } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
+                    // STRINGS RETURNED IN UTF-8 FORMAT
+                    $delim = substr($str, 0, 1);
+                    $chrs = substr($str, 1, -1);
+                    $utf8 = '';
+                    $strlen_chrs = strlen($chrs);
+
+                    for ($c = 0; $c < $strlen_chrs; ++$c) {
+
+                        $substr_chrs_c_2 = substr($chrs, $c, 2);
+                        $ord_chrs_c = ord($chrs{$c});
+
+                        switch (true) {
+                            case $substr_chrs_c_2 == '\b':
+                                $utf8 .= chr(0x08);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\t':
+                                $utf8 .= chr(0x09);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\n':
+                                $utf8 .= chr(0x0A);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\f':
+                                $utf8 .= chr(0x0C);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\r':
+                                $utf8 .= chr(0x0D);
+                                ++$c;
+                                break;
+
+                            case $substr_chrs_c_2 == '\\"':
+                            case $substr_chrs_c_2 == '\\\'':
+                            case $substr_chrs_c_2 == '\\\\':
+                            case $substr_chrs_c_2 == '\\/':
+                                if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
+                                   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
+                                    $utf8 .= $chrs{++$c};
+                                }
+                                break;
+
+                            case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
+                                // single, escaped unicode character
+                                $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
+                                       . chr(hexdec(substr($chrs, ($c + 4), 2)));
+                                $utf8 .= $this->utf162utf8($utf16);
+                                $c += 5;
+                                break;
+
+                            case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
+                                $utf8 .= $chrs{$c};
+                                break;
+
+                            case ($ord_chrs_c & 0xE0) == 0xC0:
+                                // characters U-00000080 - U-000007FF, mask 110XXXXX
+                                //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 2);
+                                ++$c;
+                                break;
+
+                            case ($ord_chrs_c & 0xF0) == 0xE0:
+                                // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 3);
+                                $c += 2;
+                                break;
+
+                            case ($ord_chrs_c & 0xF8) == 0xF0:
+                                // characters U-00010000 - U-001FFFFF, mask 11110XXX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 4);
+                                $c += 3;
+                                break;
+
+                            case ($ord_chrs_c & 0xFC) == 0xF8:
+                                // characters U-00200000 - U-03FFFFFF, mask 111110XX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 5);
+                                $c += 4;
+                                break;
+
+                            case ($ord_chrs_c & 0xFE) == 0xFC:
+                                // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 6);
+                                $c += 5;
+                                break;
+
+                        }
+
+                    }
+
+                    return $utf8;
+
+                } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
+                    // array, or object notation
+
+                    if ($str{0} == '[') {
+                        $stk = array(SERVICES_JSON_IN_ARR);
+                        $arr = array();
+                    } else {
+                        if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                            $stk = array(SERVICES_JSON_IN_OBJ);
+                            $obj = array();
+                        } else {
+                            $stk = array(SERVICES_JSON_IN_OBJ);
+                            $obj = new stdClass();
+                        }
+                    }
+
+                    array_push($stk, array('what'  => SERVICES_JSON_SLICE,
+                                           'where' => 0,
+                                           'delim' => false));
+
+                    $chrs = substr($str, 1, -1);
+                    $chrs = $this->reduce_string($chrs);
+
+                    if ($chrs == '') {
+                        if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                            return $arr;
+
+                        } else {
+                            return $obj;
+
+                        }
+                    }
+
+                    //print("\nparsing {$chrs}\n");
+
+                    $strlen_chrs = strlen($chrs);
+
+                    for ($c = 0; $c <= $strlen_chrs; ++$c) {
+
+                        $top = end($stk);
+                        $substr_chrs_c_2 = substr($chrs, $c, 2);
+
+                        if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
+                            // found a comma that is not inside a string, array, etc.,
+                            // OR we've reached the end of the character list
+                            $slice = substr($chrs, $top['where'], ($c - $top['where']));
+                            array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
+                            //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                            if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                                // we are in an array, so just push an element onto the stack
+                                array_push($arr, $this->decode($slice));
+
+                            } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+                                // we are in an object, so figure
+                                // out the property name and set an
+                                // element in an associative array,
+                                // for now
+                                $parts = array();
+                                
+                                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                    // "name":value pair
+                                    $key = $this->decode($parts[1]);
+                                    $val = $this->decode($parts[2]);
+
+                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                                        $obj[$key] = $val;
+                                    } else {
+                                        $obj->$key = $val;
+                                    }
+                                } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                    // name:value pair, where name is unquoted
+                                    $key = $parts[1];
+                                    $val = $this->decode($parts[2]);
+
+                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                                        $obj[$key] = $val;
+                                    } else {
+                                        $obj->$key = $val;
+                                    }
+                                }
+
+                            }
+
+                        } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
+                            // found a quote, and we are not inside a string
+                            array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
+                            //print("Found start of string at {$c}\n");
+
+                        } elseif (($chrs{$c} == $top['delim']) &&
+                                 ($top['what'] == SERVICES_JSON_IN_STR) &&
+                                 ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
+                            // found a quote, we're in a string, and it's not escaped
+                            // we know that it's not escaped becase there is _not_ an
+                            // odd number of backslashes at the end of the string so far
+                            array_pop($stk);
+                            //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
+
+                        } elseif (($chrs{$c} == '[') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a left-bracket, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
+                            //print("Found start of array at {$c}\n");
+
+                        } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
+                            // found a right-bracket, and we're in an array
+                            array_pop($stk);
+                            //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        } elseif (($chrs{$c} == '{') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a left-brace, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
+                            //print("Found start of object at {$c}\n");
+
+                        } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
+                            // found a right-brace, and we're in an object
+                            array_pop($stk);
+                            //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        } elseif (($substr_chrs_c_2 == '/*') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a comment start, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
+                            $c++;
+                            //print("Found start of comment at {$c}\n");
+
+                        } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
+                            // found a comment end, and we're in one now
+                            array_pop($stk);
+                            $c++;
+
+                            for ($i = $top['where']; $i <= $c; ++$i)
+                                $chrs = substr_replace($chrs, ' ', $i, 1);
+
+                            //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        }
+
+                    }
+
+                    if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                        return $arr;
+
+                    } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+                        return $obj;
+
+                    }
+
+                }
+        }
+    }
+
+    /**
+     * @todo Ultimately, this should just call PEAR::isError()
+     */
+    function isError($data, $code = null)
+    {
+        if (class_exists('pear')) {
+            return PEAR::isError($data, $code);
+        } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
+                                 is_subclass_of($data, 'services_json_error'))) {
+            return true;
+        }
+
+        return false;
+    }
+}
+
+if (class_exists('PEAR_Error')) {
+
+    class Services_JSON_Error extends PEAR_Error
+    {
+        function Services_JSON_Error($message = 'unknown error', $code = null,
+                                     $mode = null, $options = null, $userinfo = null)
+        {
+            parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
+        }
+    }
+
+} else {
+
+    /**
+     * @todo Ultimately, this class shall be descended from PEAR_Error
+     */
+    class Services_JSON_Error
+    {
+        function Services_JSON_Error($message = 'unknown error', $code = null,
+                                     $mode = null, $options = null, $userinfo = null)
+        {
+
+        }
+    }
+
+}
+
+}
+
+?>
\ No newline at end of file