From 042c61ed6de1bbc993eefe47552028157f3e1e28 Mon Sep 17 00:00:00 2001 From: Tobias Diekershoff Date: Fri, 13 Mar 2009 13:21:35 +0100 Subject: [PATCH 01/83] Piwik Analytics Plugin This is a rewrite of the Google Analytics Plugin to support Piwik. --- plugins/PiwikAnalyticsPlugin.php | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 plugins/PiwikAnalyticsPlugin.php diff --git a/plugins/PiwikAnalyticsPlugin.php b/plugins/PiwikAnalyticsPlugin.php new file mode 100644 index 0000000000..458b577fa0 --- /dev/null +++ b/plugins/PiwikAnalyticsPlugin.php @@ -0,0 +1,86 @@ +. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Plugin to use Piwik Analytics (based on the Google Analytics plugin by Evan) + * + * This plugin will spoot out the correct JavaScript spell to invoke Piwik Analytics on a page. + * + * To use this plugin please add the following three lines to your config.php +#Add Piwik Analytics +require_once('plugins/PiwikAnalyticsPlugin.php'); +$pa = new PiwikAnalyticsPlugin("example.com/piwik/","id"); + * + * exchange example.com/piwik/ with the url (without http:// or https:// !) to your + * piwik installation and make sure you don't forget the final / + * exchange id with the ID your laconica installation has in your Piwik analytics + * + * @category Plugin + * @package Laconica + * @author Tobias Diekershoff + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + * @see Event + */ + +class PiwikAnalyticsPlugin extends Plugin +{ + // the base of your Piwik installation + var $piwikroot = null; + // the Piwik Id of your laconica installation + var $piwikId = null; + + function __construct($root=null, $id=null) + { + $this->piwikroot = $root; + $this->piwikid = $id; + parent::__construct(); + } + + function onEndShowScripts($action) + { + $js1 = 'var pkBaseURL = (("https:" == document.location.protocol) ? "https://'. + $this->piwikroot.'" : "http://'.$this->piwikroot. + '"); document.write(unescape("%3Cscript src=\'" + pkBaseURL + "piwik.js\''. + ' type=\'text/javascript\'%3E%3C/script%3E"));'; + $js2 = 'piwik_action_name = ""; piwik_idsite = '.$this->piwikid. + '; piwik_url = pkBaseURL + "piwik.php"; piwik_log(piwik_action_name, piwik_idsite, piwik_url);'; + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw($js1); + $action->elementEnd('script'); + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw($js2); + $action->elementEnd('script'); + } +} \ No newline at end of file From 3081b2e31708520e6ed83041ce3ba04329d3ec90 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 16 Apr 2009 08:44:48 -0400 Subject: [PATCH 02/83] start of querybyid --- classes/User.php | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/classes/User.php b/classes/User.php index 3b9b5cd839..098381f738 100644 --- a/classes/User.php +++ b/classes/User.php @@ -444,21 +444,42 @@ class User extends Memcached_DataObject 'SELECT notice.* ' . 'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' . 'WHERE subscription.subscriber = %d '; - $order = null; + return Notice::getStream(sprintf($qry, $this->id), + 'user:notices_with_friends:' . $this->id, + $offset, $limit, $since_id, $before_id, + $order, $since); } else if ($enabled === true || ($enabled == 'transitional' && $this->inboxed == 1)) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . - 'WHERE notice_inbox.user_id = %d '; - // NOTE: we override ORDER - $order = null; + $cache = common_memcache(); + + if (!empty($cache)) { + + # Get the notices out of the cache + + $notices = $cache->get(common_cache_key($cachekey)); + + # On a cache hit, return a DB-object-like wrapper + + if ($notices !== false) { + $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); + return $wrapper; + } + } + + $inbox = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since); + + $ids = array(); + + while ($inbox->fetch()) { + $ids[] = $inbox->notice_id; + } + + $inbox->free(); + unset($inbox); + + return Notice::getStreamByIds($ids, 'user:notices_with_friends:' . $this->id); } - return Notice::getStream(sprintf($qry, $this->id), - 'user:notices_with_friends:' . $this->id, - $offset, $limit, $since_id, $before_id, - $order, $since); } function blowFavesCache() From 7196410bb0398c15fbab767a9b7cedc513e6520b Mon Sep 17 00:00:00 2001 From: Tobias Diekershoff Date: Sat, 18 Apr 2009 19:00:20 +0200 Subject: [PATCH 03/83] shortening links in notices from XMPP This patch enables shortening of links, that where send from XMPP. The problem was, that in util.php common_current_user() is not finding the user account from which is posted, so the service to shorten is not known, so no shortening at all... This patch cleans up the xmppdaemon a little bit and hard codes ur1.ca as shortening service _if_ the user is not set. Ugly but working. --- lib/util.php | 20 ++++++-------------- scripts/xmppdaemon.php | 17 +++++------------ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/lib/util.php b/lib/util.php index 675ff51f01..ab5e99593e 100644 --- a/lib/util.php +++ b/lib/util.php @@ -519,11 +519,16 @@ function common_shorten_links($text) function common_shorten_link($url, $reverse = false) { + static $url_cache = array(); if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url; $user = common_current_user(); - + if (!isset($user)) { + // common current user does not find a user when called from the XMPP daemon + // therefore we'll set one here fix, so that XMPP given URLs may be shortened + $user->urlshorteningservice = 'ur1.ca'; + } $curlh = curl_init(); curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica'); @@ -1321,16 +1326,3 @@ function common_compatible_license($from, $to) // XXX: better compatibility check needed here! return ($from == $to); } - -/** - * returns a quoted table name, if required according to config - */ -function common_database_tablename($tablename) -{ - - if(common_config('db','quote_identifiers')) { - $tablename = '"'. $tablename .'"'; - } - //table prefixes could be added here later - return $tablename; -} \ No newline at end of file diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php index ef3f8c63d8..5711f715df 100755 --- a/scripts/xmppdaemon.php +++ b/scripts/xmppdaemon.php @@ -152,11 +152,6 @@ class XMPPDaemon extends Daemon $body = preg_replace('/d[\ ]*('. $to .')[\ ]*/', '', $pl['body']); $this->add_direct($user, $body, $to, $from); } else { - $len = mb_strlen($pl['body']); - if($len > 140) { - $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len); - return; - } $this->add_notice($user, $pl); } @@ -255,15 +250,13 @@ class XMPPDaemon extends Daemon function add_notice(&$user, &$pl) { $body = trim($pl['body']); - $content_shortened = common_shorten_link($body); + $content_shortened = common_shorten_links($body); if (mb_strlen($content_shortened) > 140) { - $content = trim(mb_substr($body, 0, 140)); - $content_shortened = common_shorten_link($content); + $from = jabber_normalize_jid($pl['from']); + $this->from_site($from, "Message too long - maximum is 140 characters, you sent ".mb_strlen($content_shortened)); + return; } - else { - $content = $body; - } - $notice = Notice::saveNew($user->id, $content, 'xmpp'); + $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp'); if (is_string($notice)) { $this->log(LOG_ERR, $notice); return; From 90fb7be99a74689de0d0e2409230d8bd515ac5c3 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 18 Apr 2009 15:33:36 -0400 Subject: [PATCH 04/83] Bringing the presentation of boolean variables (favorited, truncated, profile_background_tile) and the result from friendships/exist in JSON results from the Twitter Compatible API in line with what the real Twitter API does. Currently, laconica returns text strings enclosed in quotes instead of bare Javascript booleans. This change fixes that. See http://laconi.ca/trac/ticket/1326 for one open issue related to this. --- actions/twitapifriendships.php | 6 +----- actions/twitapiusers.php | 2 +- lib/twitterapi.php | 29 ++++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/actions/twitapifriendships.php b/actions/twitapifriendships.php index c50c5e84a9..2f8250e0dc 100644 --- a/actions/twitapifriendships.php +++ b/actions/twitapifriendships.php @@ -133,11 +133,7 @@ class TwitapifriendshipsAction extends TwitterapiAction return; } - if ($user_a->isSubscribed($user_b)) { - $result = 'true'; - } else { - $result = 'false'; - } + $result = $user_a->isSubscribed($user_b); switch ($apidata['content-type']) { case 'xml': diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php index 2894b7486d..41d0d955b1 100644 --- a/actions/twitapiusers.php +++ b/actions/twitapiusers.php @@ -83,7 +83,7 @@ class TwitapiusersAction extends TwitterapiAction $twitter_user['profile_link_color'] = ''; $twitter_user['profile_sidebar_fill_color'] = ''; $twitter_user['profile_sidebar_border_color'] = ''; - $twitter_user['profile_background_tile'] = 'false'; + $twitter_user['profile_background_tile'] = false; $faves = DB_DataObject::factory('fave'); $faves->user_id = $user->id; diff --git a/lib/twitterapi.php b/lib/twitterapi.php index 6a90b4e288..caf8c07163 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -51,6 +51,26 @@ class TwitterapiAction extends Action parent::handle($args); } + /** + * Overrides XMLOutputter::element to write booleans as strings (true|false). + * See that method's documentation for more info. + * + * @param string $tag Element type or tagname + * @param array $attrs Array of element attributes, as + * key-value pairs + * @param string $content string content of the element + * + * @return void + */ + function element($tag, $attrs=null, $content=null) + { + if (is_bool($content)) { + $content = ($content ? 'true' : 'false'); + } + + return parent::element($tag, $attrs, $content); + } + function twitter_user_array($profile, $get_notice=false) { @@ -66,7 +86,7 @@ class TwitterapiAction extends Action $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE); - $twitter_user['protected'] = 'false'; # not supported by Laconica yet + $twitter_user['protected'] = false; # not supported by Laconica yet $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; if ($get_notice) { @@ -86,7 +106,7 @@ class TwitterapiAction extends Action $twitter_status = array(); $twitter_status['text'] = $notice->content; - $twitter_status['truncated'] = 'false'; # Not possible on Laconica + $twitter_status['truncated'] = false; # Not possible on Laconica $twitter_status['created_at'] = $this->date_twitter($notice->created); $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? intval($notice->reply_to) : null; @@ -108,10 +128,9 @@ class TwitterapiAction extends Action ($replier_profile) ? $replier_profile->nickname : null; if (isset($this->auth_user)) { - $twitter_status['favorited'] = - ($this->auth_user->hasFave($notice)) ? 'true' : 'false'; + $twitter_status['favorited'] = $this->auth_user->hasFave($notice); } else { - $twitter_status['favorited'] = 'false'; + $twitter_status['favorited'] = false; } if ($include_user) { From b764c3be78aef4305b75b14d7f3e8c9f1c552f9e Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 18 Apr 2009 15:42:44 -0400 Subject: [PATCH 05/83] This should change the JSON representation of the booleans 'following' and 'notifications', but unlike the previous change, I was unable to test this. --- actions/twitapiusers.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php index 41d0d955b1..92d67454cc 100644 --- a/actions/twitapiusers.php +++ b/actions/twitapiusers.php @@ -103,22 +103,14 @@ class TwitapiusersAction extends TwitterapiAction if (isset($apidata['user'])) { - if ($apidata['user']->isSubscribed($profile)) { - $twitter_user['following'] = 'true'; - } else { - $twitter_user['following'] = 'false'; - } + $twitter_user['following'] = $apidata['user']->isSubscribed($profile); // Notifications on? $sub = Subscription::pkeyGet(array('subscriber' => $apidata['user']->id, 'subscribed' => $profile->id)); if ($sub) { - if ($sub->jabber || $sub->sms) { - $twitter_user['notifications'] = 'true'; - } else { - $twitter_user['notifications'] = 'false'; - } + $twitter_user['notifications'] = ($sub->jabber || $sub->sms); } } From 48846c38057b20bb51814c22456d6907cc2a99e8 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 18 Apr 2009 15:54:23 -0400 Subject: [PATCH 06/83] Fixing spaces/tabs. --- actions/twitapiusers.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php index 92d67454cc..1542cfb33e 100644 --- a/actions/twitapiusers.php +++ b/actions/twitapiusers.php @@ -82,8 +82,8 @@ class TwitapiusersAction extends TwitterapiAction $twitter_user['profile_text_color'] = ''; $twitter_user['profile_link_color'] = ''; $twitter_user['profile_sidebar_fill_color'] = ''; - $twitter_user['profile_sidebar_border_color'] = ''; - $twitter_user['profile_background_tile'] = false; + $twitter_user['profile_sidebar_border_color'] = ''; + $twitter_user['profile_background_tile'] = false; $faves = DB_DataObject::factory('fave'); $faves->user_id = $user->id; @@ -103,16 +103,16 @@ class TwitapiusersAction extends TwitterapiAction if (isset($apidata['user'])) { - $twitter_user['following'] = $apidata['user']->isSubscribed($profile); + $twitter_user['following'] = $apidata['user']->isSubscribed($profile); - // Notifications on? - $sub = Subscription::pkeyGet(array('subscriber' => - $apidata['user']->id, 'subscribed' => $profile->id)); + // Notifications on? + $sub = Subscription::pkeyGet(array('subscriber' => + $apidata['user']->id, 'subscribed' => $profile->id)); - if ($sub) { - $twitter_user['notifications'] = ($sub->jabber || $sub->sms); - } - } + if ($sub) { + $twitter_user['notifications'] = ($sub->jabber || $sub->sms); + } + } if ($apidata['content-type'] == 'xml') { $this->init_document('xml'); From 4cb0a929806869b09d4325c1464fc2ecb7b5a76e Mon Sep 17 00:00:00 2001 From: CiaranG Date: Tue, 21 Apr 2009 20:43:57 +0100 Subject: [PATCH 07/83] Add feed2omb to notice sources --- db/notice_source.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/db/notice_source.sql b/db/notice_source.sql index d5124e223a..ce44f32354 100644 --- a/db/notice_source.sql +++ b/db/notice_source.sql @@ -8,6 +8,7 @@ VALUES ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()), ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()), ('Facebook','Facebook','http://apps.facebook.com/identica/', now()), + ('feed2omb','feed2omb','http://projects.ciarang.com/p/feed2omb/', now()), ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()), ('HelloTxt','HelloTxt','http://hellotxt.com/', now()), ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()), From ec5e06a542d6b06ea9b1d3de7cb309dd1088b4d4 Mon Sep 17 00:00:00 2001 From: CiaranG Date: Tue, 21 Apr 2009 23:36:15 +0100 Subject: [PATCH 08/83] Suppress errors when checking for the existence of files that might be restricted by open_basedir - should resolve issue #1310 --- lib/common.php | 2 +- lib/util.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/common.php b/lib/common.php index b3882d2079..f983c4d168 100644 --- a/lib/common.php +++ b/lib/common.php @@ -192,7 +192,7 @@ $_config_files[] = INSTALLDIR.'/config.php'; $_have_a_config = false; foreach ($_config_files as $_config_file) { - if (file_exists($_config_file)) { + if (@file_exists($_config_file)) { include_once($_config_file); $_have_a_config = true; } diff --git a/lib/util.php b/lib/util.php index e0eda0114b..e1dd238ba8 100644 --- a/lib/util.php +++ b/lib/util.php @@ -966,7 +966,7 @@ function common_root_url($ssl=false) function common_good_rand($bytes) { // XXX: use random.org...? - if (file_exists('/dev/urandom')) { + if (@file_exists('/dev/urandom')) { return common_urandom($bytes); } else { // FIXME: this is probably not good enough return common_mtrand($bytes); From fe3241183e3559442003b9b70435d59126e11b7e Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Thu, 23 Apr 2009 01:03:17 +0000 Subject: [PATCH 09/83] fix join a group link from user homepage when he hasn't posted anything yet --- actions/all.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/all.php b/actions/all.php index a92e554623..a53bbea07b 100644 --- a/actions/all.php +++ b/actions/all.php @@ -93,7 +93,7 @@ class AllAction extends ProfileAction if (common_logged_in()) { $current_user = common_current_user(); if ($this->user->id === $current_user->id) { - $message .= _('Try subscribing to more people, [join a group](%%action.groups) or post something yourself.'); + $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.'); } else { $message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname); } From c4941e904316ef98582290602f8cba1d220b873a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 23 Apr 2009 02:17:58 +0000 Subject: [PATCH 10/83] Added ajax responses --- actions/newmessage.php | 45 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/actions/newmessage.php b/actions/newmessage.php index 82276ff341..52d4899ba2 100644 --- a/actions/newmessage.php +++ b/actions/newmessage.php @@ -172,15 +172,54 @@ class NewmessageAction extends Action $this->notify($user, $this->other, $message); - $url = common_local_url('outbox', array('nickname' => $user->nickname)); + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _('Message sent')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'command_result'), + sprintf(_('Direct message to %s sent'), + $this->other->nickname)); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('outbox', + array('nickname' => $user->nickname)); + common_redirect($url, 303); + } + } - common_redirect($url, 303); + /** + * Show an Ajax-y error message + * + * Goes back to the browser, where it's shown in a popup. + * + * @param string $msg Message to show + * + * @return void + */ + + function ajaxErrorMsg($msg) + { + $this->startHTML('text/xml;charset=utf-8', true); + $this->elementStart('head'); + $this->element('title', null, _('Ajax Error')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'error'), $msg); + $this->elementEnd('body'); + $this->elementEnd('html'); } function showForm($msg = null) { - $this->msg = $msg; + if ($msg && $this->boolean('ajax')) { + $this->ajaxErrorMsg($msg); + return; + } + $this->msg = $msg; $this->showPage(); } From 63c52784c7acc54b0b7b39ff3ca7763774e4ab9a Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 23 Apr 2009 02:22:00 +0000 Subject: [PATCH 11/83] Allowing XHR for Inbox/Outbox pages. --- js/util.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/js/util.js b/js/util.js index 53e6eb7923..13036f7caf 100644 --- a/js/util.js +++ b/js/util.js @@ -192,10 +192,8 @@ $(document).ready(function(){ $("#notice_action-submit").removeClass("disabled"); } }; - if (document.body.id != 'inbox' && document.body.id != 'outbox') { - $("#form_notice").ajaxForm(PostNotice); - $("#form_notice").each(addAjaxHidden); - } + $("#form_notice").ajaxForm(PostNotice); + $("#form_notice").each(addAjaxHidden); NoticeHover(); NoticeReply(); }); From 640628de2d593933e810b4785dfe38923b979713 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 05:03:19 -0400 Subject: [PATCH 12/83] A queuehandler for blowing caches offline We add a queuehandler for blowing the memcached caches off-line. This should speed up the processing of new notices. --- classes/Notice.php | 25 ++++++++++++- lib/util.php | 14 ++++++-- scripts/memcachedqueuehandler.php | 58 +++++++++++++++++++++++++++++++ scripts/startdaemons.sh | 3 +- scripts/stopdaemons.sh | 2 +- 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 scripts/memcachedqueuehandler.php diff --git a/classes/Notice.php b/classes/Notice.php index 5fa0d79a16..fbfeb94899 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -207,7 +207,11 @@ class Notice extends Memcached_DataObject # XXX: someone clever could prepend instead of clearing the cache if (common_config('memcached', 'enabled')) { - $notice->blowCaches(); + if (common_config('queues', 'enabled')) { + $notice->blowAuthorCaches(); + } else { + $notice->blowCaches(); + } } return $notice; @@ -271,6 +275,25 @@ class Notice extends Memcached_DataObject $this->blowGroupCache($blowLast); } + function blowAuthorCaches($blowLast=false) + { + // Clear the user's cache + $cache = common_memcache(); + if ($cache) { + $user = User::staticGet($this->profile_id); + if (!empty($user)) { + $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); + if ($blowLast) { + $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last')); + } + } + $user->free(); + unset($user); + } + $this->blowNoticeCache($blowLast); + $this->blowPublicCache($blowLast); + } + function blowGroupCache($blowLast=false) { $cache = common_memcache(); diff --git a/lib/util.php b/lib/util.php index e0eda0114b..12797891cf 100644 --- a/lib/util.php +++ b/lib/util.php @@ -879,7 +879,17 @@ function common_broadcast_notice($notice, $remote=false) function common_enqueue_notice($notice) { - foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook', 'ping') as $transport) { + $transports = array('omb', 'sms', 'twitter', 'facebook', 'ping'); + + if (common_config('xmpp', 'enabled')) { + $transports = array_merge($transports, array('jabber', 'public')); + } + + if (common_config('memcached', 'enabled')) { + $transports[] = 'memcached'; + } + + foreach ($transports as $transport) { $qi = new Queue_item(); $qi->notice_id = $notice->id; $qi->transport = $transport; @@ -1332,7 +1342,7 @@ function common_compatible_license($from, $to) */ function common_database_tablename($tablename) { - + if(common_config('db','quote_identifiers')) { $tablename = '"'. $tablename .'"'; } diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php new file mode 100644 index 0000000000..3fcebcfc30 --- /dev/null +++ b/scripts/memcachedqueuehandler.php @@ -0,0 +1,58 @@ +#!/usr/bin/env php +. + */ + +// Abort if called from a web server + +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); +define('LACONICA', true); + +require_once(INSTALLDIR . '/lib/common.php'); + +set_error_handler('common_error_handler'); + +class MemcachedQueueHandler extends QueueHandler +{ + function transport() + { + return 'memcached'; + } + + function handle_notice($notice) + { + // XXX: fork here + common_log(LOG_INFO, "Blowing memcached for $notice->id\n"; + $notice->blowCaches(); + return true; + } +} + +ini_set("max_execution_time", "0"); +ini_set("max_input_time", "0"); +set_time_limit(0); +mb_internal_encoding('UTF-8'); + +$handler = new MemcachedQueueHandler($resource); + +$handler->runOnce(); diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh index c3729761d0..08de6d954a 100755 --- a/scripts/startdaemons.sh +++ b/scripts/startdaemons.sh @@ -24,7 +24,8 @@ DIR=`dirname $0` for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \ xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \ - twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php; do + twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php \ + memcachedqueuehandler.php; do echo -n "Starting $f..."; php $DIR/$f diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index 2bb8f9ecb2..e5a181cd12 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -24,7 +24,7 @@ SDIR=`dirname $0` DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ - xmppconfirmhandler xmppdaemon twitterhandler facebookhandler ; do + xmppconfirmhandler xmppdaemon twitterhandler facebookhandler memcachedhandler; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do From aee45ea91dcec4736d8b9befe17e030b873d9226 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 05:08:48 -0400 Subject: [PATCH 13/83] Add an inbox queue handler Handle distributing a notice to multiple inboxes in a queue handler rather than in the Web action. --- classes/Notice.php | 5 ++- lib/util.php | 4 +++ scripts/inboxqueuehandler.php | 57 +++++++++++++++++++++++++++++++++++ scripts/startdaemons.sh | 2 +- scripts/stopdaemons.sh | 3 +- 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 scripts/inboxqueuehandler.php diff --git a/classes/Notice.php b/classes/Notice.php index fbfeb94899..ff00f2a946 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -197,7 +197,10 @@ class Notice extends Memcached_DataObject $notice->saveTags(); $notice->saveGroups(); - $notice->addToInboxes(); + if (!common_config('queues', 'enabled')) { + $notice->addToInboxes(); + } + $notice->query('COMMIT'); Event::handle('EndNoticeSave', array($notice)); diff --git a/lib/util.php b/lib/util.php index 12797891cf..9b6d2941a0 100644 --- a/lib/util.php +++ b/lib/util.php @@ -889,6 +889,10 @@ function common_enqueue_notice($notice) $transports[] = 'memcached'; } + if (common_config('queues', 'enabled')) { + $transports[] = 'inbox'; + } + foreach ($transports as $transport) { $qi = new Queue_item(); $qi->notice_id = $notice->id; diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php new file mode 100644 index 0000000000..16e334b83e --- /dev/null +++ b/scripts/inboxqueuehandler.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +. + */ + +// Abort if called from a web server + +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); +define('LACONICA', true); + +require_once(INSTALLDIR . '/lib/common.php'); + +set_error_handler('common_error_handler'); + +class InboxQueueHandler extends QueueHandler +{ + function transport() + { + return 'inbox'; + } + + function handle_notice($notice) + { + common_log(LOG_INFO, "Distributing notice to inboxes for $notice->id"); + $notice->addToInboxes(); + return true; + } +} + +ini_set("max_execution_time", "0"); +ini_set("max_input_time", "0"); +set_time_limit(0); +mb_internal_encoding('UTF-8'); + +$handler = new InboxQueueHandler($resource); + +$handler->runOnce(); diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh index 08de6d954a..66f9ed4e0c 100755 --- a/scripts/startdaemons.sh +++ b/scripts/startdaemons.sh @@ -25,7 +25,7 @@ DIR=`dirname $0` for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \ xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \ twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php \ - memcachedqueuehandler.php; do + memcachedqueuehandler.php inboxqueuehandler.php; do echo -n "Starting $f..."; php $DIR/$f diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index e5a181cd12..196991de0f 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -24,7 +24,8 @@ SDIR=`dirname $0` DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ - xmppconfirmhandler xmppdaemon twitterhandler facebookhandler memcachedhandler; do + xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ + memcachedhandler inboxhandler; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do From 2053bdabef929f4a095d91fbdd3fa62646e9f332 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 05:23:59 -0400 Subject: [PATCH 14/83] fix parse error in memcachedqueuehandler --- scripts/memcachedqueuehandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php index 3fcebcfc30..43231fa2c6 100644 --- a/scripts/memcachedqueuehandler.php +++ b/scripts/memcachedqueuehandler.php @@ -42,7 +42,7 @@ class MemcachedQueueHandler extends QueueHandler function handle_notice($notice) { // XXX: fork here - common_log(LOG_INFO, "Blowing memcached for $notice->id\n"; + common_log(LOG_INFO, "Blowing memcached for $notice->id"); $notice->blowCaches(); return true; } From 31b04220bd9fbadbfac01b60276a9c14896b6a45 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 09:34:56 +0000 Subject: [PATCH 15/83] incorrect config setting for queues --- classes/Notice.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index ff00f2a946..ebed0b8af5 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -197,7 +197,7 @@ class Notice extends Memcached_DataObject $notice->saveTags(); $notice->saveGroups(); - if (!common_config('queues', 'enabled')) { + if (!common_config('queue', 'enabled')) { $notice->addToInboxes(); } @@ -210,7 +210,7 @@ class Notice extends Memcached_DataObject # XXX: someone clever could prepend instead of clearing the cache if (common_config('memcached', 'enabled')) { - if (common_config('queues', 'enabled')) { + if (common_config('queue', 'enabled')) { $notice->blowAuthorCaches(); } else { $notice->blowCaches(); From ece70bf326fa680ec7acdef00751e2bb71bceee1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 09:35:10 +0000 Subject: [PATCH 16/83] incorrect config setting for inboxes --- lib/util.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/util.php b/lib/util.php index af4db4f02d..d77039b747 100644 --- a/lib/util.php +++ b/lib/util.php @@ -889,7 +889,8 @@ function common_enqueue_notice($notice) $transports[] = 'memcached'; } - if (common_config('queues', 'enabled')) { + if (common_config('inboxes', 'enabled') === true || + common_config('inboxes', 'enabled') === 'transitional') { $transports[] = 'inbox'; } From a3e727823d01ed9fd5f440eeaf27449092309865 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 09:52:21 +0000 Subject: [PATCH 17/83] some basic fixes for inbox and memcached queue handlers --- scripts/inboxqueuehandler.php | 15 +++++++++++++-- scripts/memcachedqueuehandler.php | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) mode change 100644 => 100755 scripts/inboxqueuehandler.php mode change 100644 => 100755 scripts/memcachedqueuehandler.php diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php old mode 100644 new mode 100755 index 16e334b83e..c76b803898 --- a/scripts/inboxqueuehandler.php +++ b/scripts/inboxqueuehandler.php @@ -29,6 +29,7 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); define('LACONICA', true); require_once(INSTALLDIR . '/lib/common.php'); +require_once(INSTALLDIR . '/lib/queuehandler.php'); set_error_handler('common_error_handler'); @@ -39,12 +40,20 @@ class InboxQueueHandler extends QueueHandler return 'inbox'; } + function start() { + $this->log(LOG_INFO, "INITIALIZE"); + return true; + } + function handle_notice($notice) { - common_log(LOG_INFO, "Distributing notice to inboxes for $notice->id"); + $this->log(LOG_INFO, "Distributing notice to inboxes for $notice->id"); $notice->addToInboxes(); return true; } + + function finish() { + } } ini_set("max_execution_time", "0"); @@ -52,6 +61,8 @@ ini_set("max_input_time", "0"); set_time_limit(0); mb_internal_encoding('UTF-8'); -$handler = new InboxQueueHandler($resource); +$id = ($argc > 1) ? $argv[1] : null; + +$handler = new InboxQueueHandler($id); $handler->runOnce(); diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php old mode 100644 new mode 100755 index 43231fa2c6..6e819b41f1 --- a/scripts/memcachedqueuehandler.php +++ b/scripts/memcachedqueuehandler.php @@ -29,6 +29,7 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); define('LACONICA', true); require_once(INSTALLDIR . '/lib/common.php'); +require_once(INSTALLDIR . '/lib/queuehandler.php'); set_error_handler('common_error_handler'); @@ -39,13 +40,22 @@ class MemcachedQueueHandler extends QueueHandler return 'memcached'; } + function start() { + $this->log(LOG_INFO, "INITIALIZE"); + return true; + } + function handle_notice($notice) { // XXX: fork here - common_log(LOG_INFO, "Blowing memcached for $notice->id"); + $this->log(LOG_INFO, "Blowing memcached for $notice->id"); $notice->blowCaches(); return true; } + + function finish() { + } + } ini_set("max_execution_time", "0"); @@ -53,6 +63,8 @@ ini_set("max_input_time", "0"); set_time_limit(0); mb_internal_encoding('UTF-8'); -$handler = new MemcachedQueueHandler($resource); +$id = ($argc > 1) ? $argv[1] : null; + +$handler = new MemcachedQueueHandler($id); $handler->runOnce(); From 7c383dc1d46cae8abc6d1e5c7eb93ad7cdd91f63 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 10:08:26 +0000 Subject: [PATCH 18/83] alert to what transport we're checking for --- lib/queuehandler.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/queuehandler.php b/lib/queuehandler.php index 9ce9e32b3b..fde650d9ed 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -36,7 +36,7 @@ class QueueHandler extends Daemon $this->set_id($id); } } - + function class_name() { return ucfirst($this->transport()) . 'Handler'; @@ -46,7 +46,7 @@ class QueueHandler extends Daemon { return strtolower($this->class_name().'.'.$this->get_id()); } - + function get_id() { return $this->_id; @@ -56,16 +56,16 @@ class QueueHandler extends Daemon { $this->_id = $id; } - + function transport() { return null; } - + function start() { } - + function finish() { } @@ -74,14 +74,14 @@ class QueueHandler extends Daemon { return true; } - + function run() { if (!$this->start()) { return false; } - $this->log(LOG_INFO, 'checking for queued notices'); $transport = $this->transport(); + $this->log(LOG_INFO, 'checking for queued notices for "' . $transport . '"'); do { $qi = Queue_item::top($transport); if ($qi) { @@ -113,7 +113,7 @@ class QueueHandler extends Daemon } else { $this->clear_old_claims(); $this->idle(5); - } + } } while (true); if (!$this->finish()) { return false; @@ -127,7 +127,7 @@ class QueueHandler extends Daemon sleep($timeout); } } - + function clear_old_claims() { $qi = new Queue_item(); @@ -137,10 +137,9 @@ class QueueHandler extends Daemon $qi->free(); unset($qi); } - + function log($level, $msg) { common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); } } - \ No newline at end of file From 290ae7888c4d8dbcb703920960b10bca635563ed Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 10:08:51 +0000 Subject: [PATCH 19/83] blow subs cache after updating inboxes --- scripts/inboxqueuehandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php index c76b803898..73d31e8542 100755 --- a/scripts/inboxqueuehandler.php +++ b/scripts/inboxqueuehandler.php @@ -49,6 +49,7 @@ class InboxQueueHandler extends QueueHandler { $this->log(LOG_INFO, "Distributing notice to inboxes for $notice->id"); $notice->addToInboxes(); + $notice->blowSubsCache(); return true; } From 1c0d82de3bb7f75649a017a7d5632a6e070876c2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 10:09:08 +0000 Subject: [PATCH 20/83] 8-char limit on transports --- lib/util.php | 3 ++- scripts/memcachedqueuehandler.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/util.php b/lib/util.php index d77039b747..f862d7fcc2 100644 --- a/lib/util.php +++ b/lib/util.php @@ -886,7 +886,8 @@ function common_enqueue_notice($notice) } if (common_config('memcached', 'enabled')) { - $transports[] = 'memcached'; + // Note: limited to 8 chars + $transports[] = 'memcache'; } if (common_config('inboxes', 'enabled') === true || diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php index 6e819b41f1..185b781f75 100755 --- a/scripts/memcachedqueuehandler.php +++ b/scripts/memcachedqueuehandler.php @@ -37,7 +37,7 @@ class MemcachedQueueHandler extends QueueHandler { function transport() { - return 'memcached'; + return 'memcache'; } function start() { From a9df5eab100bce97da3e028851d224ea2e9fff80 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 23 Apr 2009 10:38:51 +0000 Subject: [PATCH 21/83] insert into user's inbox at Web time --- classes/Notice.php | 76 +++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index ebed0b8af5..27b98de1cc 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -197,7 +197,9 @@ class Notice extends Memcached_DataObject $notice->saveTags(); $notice->saveGroups(); - if (!common_config('queue', 'enabled')) { + if (common_config('queue', 'enabled')) { + $notice->addToAuthorInbox(); + } else { $notice->addToInboxes(); } @@ -282,16 +284,8 @@ class Notice extends Memcached_DataObject { // Clear the user's cache $cache = common_memcache(); - if ($cache) { - $user = User::staticGet($this->profile_id); - if (!empty($user)) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); - if ($blowLast) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last')); - } - } - $user->free(); - unset($user); + if (!empty($cache)) { + $cache->delete(common_cache_key('user:notices_with_friends:' . $this->profile_id)); } $this->blowNoticeCache($blowLast); $this->blowPublicCache($blowLast); @@ -665,6 +659,33 @@ class Notice extends Memcached_DataObject return; } + function addToAuthorInbox() + { + $enabled = common_config('inboxes', 'enabled'); + + if ($enabled === true || $enabled === 'transitional') { + $user = User::staticGet('id', $this->profile_id); + if (empty($user)) { + return; + } + $inbox = new Notice_inbox(); + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; + $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' . + "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " . + "FROM $UT " . + "WHERE $UT.id = " . $this->profile_id . ' ' . + 'AND NOT EXISTS (SELECT user_id, notice_id ' . + 'FROM notice_inbox ' . + "WHERE user_id = " . $this->profile_id . ' '. + 'AND notice_id = ' . $this->id . ' )'; + if ($enabled === 'transitional') { + $qry .= " AND $UT.inboxed = 1"; + } + $inbox->query($qry); + } + return; + } + function saveGroups() { $enabled = common_config('inboxes', 'enabled'); @@ -717,24 +738,29 @@ class Notice extends Memcached_DataObject // FIXME: do this in an offline daemon - $inbox = new Notice_inbox(); - $UT = common_config('db','type')=='pgsql'?'"user"':'user'; - $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' . - "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " . - "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " . - 'WHERE group_member.group_id = ' . $group->id . ' ' . - 'AND NOT EXISTS (SELECT user_id, notice_id ' . - 'FROM notice_inbox ' . - "WHERE user_id = $UT.id " . - 'AND notice_id = ' . $this->id . ' )'; - if ($enabled === 'transitional') { - $qry .= " AND $UT.inboxed = 1"; - } - $result = $inbox->query($qry); + $this->addToGroupInboxes($group); } } } + function addToGroupInboxes($group) + { + $inbox = new Notice_inbox(); + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; + $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' . + "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " . + "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " . + 'WHERE group_member.group_id = ' . $group->id . ' ' . + 'AND NOT EXISTS (SELECT user_id, notice_id ' . + 'FROM notice_inbox ' . + "WHERE user_id = $UT.id " . + 'AND notice_id = ' . $this->id . ' )'; + if ($enabled === 'transitional') { + $qry .= " AND $UT.inboxed = 1"; + } + $result = $inbox->query($qry); + } + function saveReplies() { // Alternative reply format From 2bdf192dabb9dfbbc889c3387bf5261a2d4166ce Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 23 Apr 2009 21:35:21 +0000 Subject: [PATCH 22/83] XHR alerts for server-side errors: 404, 502, 503, 504. There is also a 7 second timeout if the server doesn't get back with a response. --- js/util.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/js/util.js b/js/util.js index 13036f7caf..c7b1272c8d 100644 --- a/js/util.js +++ b/js/util.js @@ -166,6 +166,25 @@ $(document).ready(function(){ $("#notice_action-submit").addClass("disabled"); return true; }, + timeout: 1000, + error: function (xhr, textStatus, errorThrown) { $("#form_notice").removeClass("processing"); + $("#notice_action-submit").removeAttr("disabled"); + $("#notice_action-submit").removeClass("disabled"); + + if (textStatus == "timeout") { + alert ("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists"); + } + else { + switch(xhr.status) { + default: case 404: + alert("Sorry! We had trouble sending your notice. Please report the problem to the site administrator if this happens again."); + break; + case 502: case 503: case 504: + alert("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists."); + break; + } + } + }, success: function(xml) { if ($("#error", xml).length > 0) { var result = document._importNode($("p", xml).get(0), true); result = result.textContent || result.innerHTML; From 83ba1b0b5ef10dc1101ba35bcde4710fd13179cc Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 23 Apr 2009 21:51:49 +0000 Subject: [PATCH 23/83] The real 7 seconds (7000) timeout for XHR posting. --- js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/util.js b/js/util.js index c7b1272c8d..16422df579 100644 --- a/js/util.js +++ b/js/util.js @@ -166,7 +166,7 @@ $(document).ready(function(){ $("#notice_action-submit").addClass("disabled"); return true; }, - timeout: 1000, + timeout: 7000, error: function (xhr, textStatus, errorThrown) { $("#form_notice").removeClass("processing"); $("#notice_action-submit").removeAttr("disabled"); $("#notice_action-submit").removeClass("disabled"); From 12867d114d8d40bf9a45b3e1eb9b0e50d736d333 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 24 Apr 2009 17:22:43 +0000 Subject: [PATCH 24/83] UI for server errors. --- js/util.js | 15 +++------------ theme/base/css/display.css | 11 +++++++++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/js/util.js b/js/util.js index 16422df579..15a14625c7 100644 --- a/js/util.js +++ b/js/util.js @@ -166,23 +166,14 @@ $(document).ready(function(){ $("#notice_action-submit").addClass("disabled"); return true; }, - timeout: 7000, error: function (xhr, textStatus, errorThrown) { $("#form_notice").removeClass("processing"); $("#notice_action-submit").removeAttr("disabled"); $("#notice_action-submit").removeClass("disabled"); - - if (textStatus == "timeout") { - alert ("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists"); + if ($(".error", xhr.responseXML).length > 0) { + $('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true)); } else { - switch(xhr.status) { - default: case 404: - alert("Sorry! We had trouble sending your notice. Please report the problem to the site administrator if this happens again."); - break; - case 502: case 503: case 504: - alert("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists."); - break; - } + alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again."); } }, success: function(xml) { if ($("#error", xml).length > 0) { diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 2fb1c007fc..0bc2e68b65 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -86,7 +86,7 @@ border:0; .error, .success { -padding:4px 7px; +padding:4px 1.55%; border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; @@ -426,6 +426,7 @@ line-height:1; #form_notice fieldset { border:0; padding:0; +position:relative; } #form_notice legend { display:none; @@ -480,7 +481,13 @@ margin-bottom:7px; margin-left:18px; float:left; } - +#form_notice .error { +float:left; +clear:both; +width:96.9%; +margin-bottom:0; +line-height:1.618; +} /* entity_profile */ .entity_profile { From ecb09fb8646def7f6a7c5fc0fc2d4df6676edd06 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 24 Apr 2009 13:31:03 -0400 Subject: [PATCH 25/83] check for existence of xmlrpc extension in LinkbackPlugin --- plugins/LinkbackPlugin.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/LinkbackPlugin.php b/plugins/LinkbackPlugin.php index 881ead99ec..93a0294c4c 100644 --- a/plugins/LinkbackPlugin.php +++ b/plugins/LinkbackPlugin.php @@ -121,6 +121,12 @@ class LinkbackPlugin extends Plugin { $args = array($this->notice->uri, $url); + if (!extension_loaded('xmlrpc')) { + if (!dl('xmlrpc.so')) { + common_log(LOG_ERR, "Can't pingback; xmlrpc extension not available."); + } + } + $request = xmlrpc_encode_request('pingback.ping', $args); $context = stream_context_create(array('http' => array('method' => "POST", 'header' => @@ -141,7 +147,7 @@ class LinkbackPlugin extends Plugin } // Largely cadged from trackback_cls.php by - // Ran Aroussi , GPL2 + // Ran Aroussi , GPL2 or any later version // http://phptrackback.sourceforge.net/ function getTrackback($text, $url) From c008c0d4a56ec265ba6e10d208f9954510296f12 Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Fri, 24 Apr 2009 20:01:03 +0000 Subject: [PATCH 26/83] fixed trac#1215, 1216, 1217 and 1219: subscribers/subscriptions people tagclouds. --- lib/subpeopletagcloudsection.php | 1 + lib/subscriberspeopleselftagcloudsection.php | 10 +++++++++- lib/subscriberspeopletagcloudsection.php | 5 +++-- lib/subscriptionspeopleselftagcloudsection.php | 9 ++++++++- lib/subscriptionspeopletagcloudsection.php | 4 +++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/subpeopletagcloudsection.php b/lib/subpeopletagcloudsection.php index d98f28afa7..9f6948dc92 100644 --- a/lib/subpeopletagcloudsection.php +++ b/lib/subpeopletagcloudsection.php @@ -74,3 +74,4 @@ class SubPeopleTagCloudSection extends TagCloudSection $this->out->elementEnd('li'); } } + diff --git a/lib/subscriberspeopleselftagcloudsection.php b/lib/subscriberspeopleselftagcloudsection.php index b5a39c6de6..115241a53f 100644 --- a/lib/subscriberspeopleselftagcloudsection.php +++ b/lib/subscriberspeopleselftagcloudsection.php @@ -49,6 +49,14 @@ class SubscribersPeopleSelfTagCloudSection extends SubPeopleTagCloudSection } function query() { - return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc'; +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc'; + + + return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + + } } + diff --git a/lib/subscriberspeopletagcloudsection.php b/lib/subscriberspeopletagcloudsection.php index 23011efdd3..4dafbc1c7d 100644 --- a/lib/subscriberspeopletagcloudsection.php +++ b/lib/subscriberspeopletagcloudsection.php @@ -53,8 +53,9 @@ class SubscribersPeopleTagCloudSection extends SubPeopleTagCloudSection return common_local_url('subscribers', array('nickname' => $nickname, 'tag' => $tag)); } - function query() { - return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc'; +// return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc'; + return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed and tag is not null group by tag order by weight desc'; } } + diff --git a/lib/subscriptionspeopleselftagcloudsection.php b/lib/subscriptionspeopleselftagcloudsection.php index 8ac65adb05..3896294d28 100644 --- a/lib/subscriptionspeopleselftagcloudsection.php +++ b/lib/subscriptionspeopleselftagcloudsection.php @@ -49,6 +49,13 @@ class SubscriptionsPeopleSelfTagCloudSection extends SubPeopleTagCloudSection } function query() { - return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc'; +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc'; + + + + return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; } } + diff --git a/lib/subscriptionspeopletagcloudsection.php b/lib/subscriptionspeopletagcloudsection.php index c3f7d1763e..f9c8672e36 100644 --- a/lib/subscriptionspeopletagcloudsection.php +++ b/lib/subscriptionspeopletagcloudsection.php @@ -54,6 +54,8 @@ class SubscriptionsPeopleTagCloudSection extends SubPeopleTagCloudSection } function query() { - return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc'; +// return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc'; + return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed and tag is not null group by tag order by weight desc'; } } + From d71fbe9d9693cd5426be74807ff8f18fc6376c56 Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Fri, 24 Apr 2009 20:28:39 +0000 Subject: [PATCH 27/83] fixed subscriptions dropdown action --- lib/galleryaction.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/galleryaction.php b/lib/galleryaction.php index 0484918ce2..8fa11a7562 100644 --- a/lib/galleryaction.php +++ b/lib/galleryaction.php @@ -134,9 +134,11 @@ class GalleryAction extends Action $this->elementStart('li', array('id'=>'filter_tags_item')); $this->elementStart('form', array('name' => 'bytag', 'id' => 'bytag', + 'action' => common_path('?action=' . $this->trimmed('action')), 'method' => 'post')); $this->dropdown('tag', _('Tag'), $content, _('Choose a tag to narrow list'), false, $tag); + $this->hidden('nickname', $this->user->nickname); $this->submit('submit', _('Go')); $this->elementEnd('form'); $this->elementEnd('li'); @@ -169,4 +171,4 @@ class GalleryAction extends Action { return array(); } -} \ No newline at end of file +} From 5e6eb27f843a22b80ac114f382682fba0c37589e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 25 Apr 2009 14:20:24 -0400 Subject: [PATCH 28/83] first pass at Comet plugin; doesn't yet update --- plugins/Comet/CometPlugin.php | 138 +++++++++++ plugins/Comet/bayeux.class.inc.php | 129 ++++++++++ plugins/Comet/bayeux.class.inc.phps | 123 ++++++++++ plugins/Comet/jquery.comet.js | 363 ++++++++++++++++++++++++++++ plugins/Comet/updatetimeline.js | 3 + 5 files changed, 756 insertions(+) create mode 100644 plugins/Comet/CometPlugin.php create mode 100644 plugins/Comet/bayeux.class.inc.php create mode 100644 plugins/Comet/bayeux.class.inc.phps create mode 100644 plugins/Comet/jquery.comet.js create mode 100644 plugins/Comet/updatetimeline.js diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php new file mode 100644 index 0000000000..10f8c198c3 --- /dev/null +++ b/plugins/Comet/CometPlugin.php @@ -0,0 +1,138 @@ +. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Plugin to do realtime updates using Comet + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class CometPlugin extends Plugin +{ + var $server = null; + + function __construct($server=null) + { + $this->server = $server; + + parent::__construct(); + } + + function onEndShowScripts($action) + { + $timeline = null; + + switch ($action->trimmed('action')) { + case 'public': + $timeline = '/timelines/public'; + break; + default: + return true; + } + + $action->element('script', array('type' => 'text/javascript', + 'src' => common_path('plugins/Comet/jquery.comet.js')), + ' '); + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw("var _timelineServer = \"$this->server\"; ". + "var _timeline = \"$timeline\";"); + $action->elementEnd('script'); + $action->element('script', array('type' => 'text/javascript', + 'src' => common_path('plugins/Comet/updatetimeline.js')), + ' '); + return true; + } + + function onEndNoticeSave($notice) + { + $this->log(LOG_INFO, "Called for save notice."); + + $timelines = array(); + + // XXX: Add other timelines; this is just for the public one + + if ($notice->is_local || + ($notice->is_local == 0 && !common_config('public', 'localonly'))) { + $timelines[] = '/timelines/public'; + } + + if (count($timelines) > 0) { + // Require this, since we need it + require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php'); + + $json = $this->noticeAsJson($notice); + + $this->log(LOG_DEBUG, "JSON = '$json'"); + + // Bayeux? Comet? Huh? These terms confuse me + $bay = new Bayeux($this->server); + + foreach ($timelines as $timeline) { + $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'."); + $bay->publish($timeline, $json); + $this->log(LOG_DEBUG, "Done posting notice $notice->id to '$timeline'."); + } + + $bay = NULL; + } + + $this->log(LOG_DEBUG, "All done."); + return true; + } + + function noticeAsJson($notice) + { + // FIXME: this code should be abstracted to a neutral third + // party, like Notice::asJson(). I'm not sure of the ethics + // of refactoring from within a plugin, so I'm just abusing + // the TwitterApiAction method. Don't do this unless you're me! + + require_once(INSTALLDIR.'/lib/twitterapi.php'); + + $act = new TwitterApiAction('/dev/null'); + + $arr = $act->twitter_status_array($notice, true); + return $arr; + } + + // Push this up to Plugin + + function log($level, $msg) + { + common_log($level, get_class($this) . ': '.$msg); + } +} diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php new file mode 100644 index 0000000000..602a7b6446 --- /dev/null +++ b/plugins/Comet/bayeux.class.inc.php @@ -0,0 +1,129 @@ + http://morglog.alleycatracing.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +class Bayeux +{ + private $oCurl = ''; + private $nNextId = 0; + + public $sUrl = ''; + + function __construct($sUrl) + { + $this->sUrl = $sUrl; + + $this->oCurl = curl_init(); + + $aHeaders = array(); + $aHeaders[] = 'Connection: Keep-Alive'; + + curl_setopt($this->oCurl, CURLOPT_URL, $sUrl); + curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders); + curl_setopt($this->oCurl, CURLOPT_HEADER, 0); + curl_setopt($this->oCurl, CURLOPT_POST, 1); + curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); + + $this->handShake(); + } + + function __destruct() + { + $this->disconnect(); + } + + function handShake() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/handshake'; + $msgHandshake['version'] = "1.0"; + $msgHandshake['minimumVersion'] = "0.9"; + $msgHandshake['supportedConnectionTypes'] = array('long-polling'); + $msgHandshake['id'] = $this->nNextId++; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + $data = curl_exec($this->oCurl); + + if(curl_errno($this->oCurl)) + die("Error: " . curl_error($this->oCurl)); + + $oReturn = json_decode($data); + + common_debug(print_r($oReturn, true)); + + if (is_array($oReturn)) { + $oReturn = $oReturn[0]; + } + + $bSuccessful = ($oReturn->successful) ? true : false; + + if($bSuccessful) + { + $this->clientId = $oReturn->clientId; + + $this->connect(); + } + } + + public function connect() + { + $aMsg['channel'] = '/meta/connect'; + $aMsg['id'] = $this->nNextId++; + $aMsg['clientId'] = $this->clientId; + $aMsg['connectionType'] = 'long-polling'; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); + } + + function disconnect() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/disconnect'; + $msgHandshake['id'] = $this->nNextId++; + $msgHandshake['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + curl_exec($this->oCurl); + } + + public function publish($sChannel, $oData) + { + if(!$sChannel || !$oData) + return; + + $aMsg = array(); + + $aMsg['channel'] = $sChannel; + $aMsg['id'] = $this->nNextId++; + $aMsg['data'] = $oData; + $aMsg['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); +// var_dump($data); + } +} diff --git a/plugins/Comet/bayeux.class.inc.phps b/plugins/Comet/bayeux.class.inc.phps new file mode 100644 index 0000000000..ea004a4532 --- /dev/null +++ b/plugins/Comet/bayeux.class.inc.phps @@ -0,0 +1,123 @@ + http://morglog.alleycatracing.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +class Bayeux + { + private $oCurl = ''; + private $nNextId = 0; + + public $sUrl = ''; + + function __construct($sUrl) + { + $this->sUrl = $sUrl; + + $this->oCurl = curl_init(); + + $aHeaders = array(); + $aHeaders[] = 'Connection: Keep-Alive'; + + curl_setopt($this->oCurl, CURLOPT_URL, $sUrl); + curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders); + curl_setopt($this->oCurl, CURLOPT_HEADER, 0); + curl_setopt($this->oCurl, CURLOPT_POST, 1); + curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); + + $this->handShake(); + } + + function __destruct() + { + $this->disconnect(); + } + + function handShake() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/handshake'; + $msgHandshake['version'] = "1.0"; + $msgHandshake['minimumVersion'] = "0.9"; + $msgHandshake['id'] = $this->nNextId++; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + $data = curl_exec($this->oCurl); + + if(curl_errno($this->oCurl)) + die("Error: " . curl_error($this->oCurl)); + + $oReturn = json_decode($data); + $oReturn = $oReturn[0]; + + $bSuccessful = ($oReturn->successful) ? true : false; + + if($bSuccessful) + { + $this->clientId = $oReturn->clientId; + + $this->connect(); + } + } + + public function connect() + { + $aMsg['channel'] = '/meta/connect'; + $aMsg['id'] = $this->nNextId++; + $aMsg['clientId'] = $this->clientId; + $aMsg['connectionType'] = 'long-polling'; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); + } + + function disconnect() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/disconnect'; + $msgHandshake['id'] = $this->nNextId++; + $msgHandshake['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + curl_exec($this->oCurl); + } + + public function publish($sChannel, $oData) + { + if(!$sChannel || !$oData) + return; + + $aMsg = array(); + + $aMsg['channel'] = $sChannel; + $aMsg['id'] = $this->nNextId++; + $aMsg['data'] = $oData; + $aMsg['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); +// var_dump($data); + } + } diff --git a/plugins/Comet/jquery.comet.js b/plugins/Comet/jquery.comet.js new file mode 100644 index 0000000000..2124e882cb --- /dev/null +++ b/plugins/Comet/jquery.comet.js @@ -0,0 +1,363 @@ +(function($) +{ + var msgHandshake = + { + version: '1.0', + minimumVersion: '0.9', + channel: '/meta/handshake' + }; + + var oTransport = function() + { + this._bXD = + (($.comet._sUrl.substring(0,4) == 'http') && ($.comet._sUrl.substr(7,location.href.length).replace(/\/.*/, '') != location.host)) + ? + true + :false; + + this.connectionType = (this._bXD) ? 'callback-polling' : 'long-polling'; + + this.startup = function(oReturn) + { + if(this._comet._bConnected) return; + this.tunnelInit(); + }; + + this.tunnelInit = function() + { + var msgConnect = + { + channel: '/meta/connect', + clientId: $.comet.clientId, + id: String($.comet._nNextId++), + connectionType: $.comet._oTransport.connectionType + }; + + this.openTunnel(msgConnect); + }; + + this.openTunnel = function(oMsg) + { + $.comet._bPolling = true; + + this._send($.comet._sUrl, oMsg, function(sReturn) + { + var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')) : sReturn; + $.comet._bPolling = false; + $.comet.deliver(oReturn); + $.comet._oTransport.closeTunnel(); + }); + }; + + this.closeTunnel = function() + { + if(!$.comet._bInitialized) return; + + if($.comet._advice) + { + if($.comet._advice.reconnect == 'none') return; + + if($.comet._advice.interval > 0) + { + setTimeout($.comet._oTransport._connect, $.comet._advice.interval); + } + else + { + $.comet._oTransport._connect(); + } + } + else + { + $.comet._oTransport._connect(); + } + }; + + this._connect = function() + { + if(!$.comet._bInitialized) return; + + if($.comet._bPolling) return; + + if($.comet._advice && $.comet._advice.reconnect == 'handshake') + { + $.comet._bConnected = false; + $.comet.init($.comet._sUrl); + } + else if($.comet._bConnected) + { + var msgConnect = + { + //jsonp: 'test', + clientId: $.comet.clientId, + id: String($.comet._nNextId++), + channel: '/meta/connect', + connectionType: $.comet._oTransport.connectionType + }; + $.comet._oTransport.openTunnel(msgConnect); + } + }; + + this._send = function(sUrl, oMsg, fCallback) { + //default callback will check advice, deliver messages, and reconnect + var fCallback = (fCallback) ? fCallback : function(sReturn) + { + var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')) : sReturn; + + $.comet.deliver(oReturn); + + if($.comet._advice) + { + if($.comet._advice.reconnect == 'none') + return; + + if($.comet._advice.interval > 0) + { + setTimeout($.comet._oTransport._connect, $.comet._advice.interval); + } + else + { + $.comet._oTransport._connect(); + } + } + else + { + $.comet._oTransport._connect(); + } + }; + + //regular AJAX for same domain calls + if((!this._bXD) && (this.connectionType == 'long-polling')) + { + this._pollRequest = $.ajax({ + url: sUrl, + type: 'post', + beforeSend: function(oXhr) { oXhr.setRequestHeader('Connection', 'Keep-Alive'); }, + data: { message: JSON.stringify(oMsg) }, + success: fCallback + }); + } + else // JSONP callback for cross domain + { + this._pollRequest = $.ajax({ + url: sUrl, + dataType: 'jsonp', + jsonp: 'jsonp', + beforeSend: function(oXhr) { oXhr.setRequestHeader('Connection', 'Keep-Alive'); }, + data: + { + message: JSON.stringify($.extend(oMsg,{connectionType: 'callback-polling' })) + }, + success: fCallback + }); + } + } + }; + + $.comet = new function() + { + this.CONNECTED = 'CONNECTED'; + this.CONNECTING = 'CONNECTING'; + this.DISCONNECTED = 'DISCONNECTED'; + this.DISCONNECTING = 'DISCONNECTING'; + + this._aMessageQueue = []; + this._aSubscriptions = []; + this._aSubscriptionCallbacks = []; + this._bInitialized = false; + this._bConnected = false; + this._nBatch = 0; + this._nNextId = 0; + // just define the transport, do not assign it yet. + this._oTransport = ''; //oTransport; + this._sUrl = ''; + + this.supportedConectionTypes = [ 'long-polling', 'callback-polling' ]; + + this.clientId = ''; + + this._bTrigger = true; // this sends $.event.trigger(channel, data) + + this.init = function(sUrl) + { + this._sUrl = (sUrl) ? sUrl : '/cometd'; + + this._oTransport = new oTransport(); + + this._aMessageQueue = []; + this._aSubscriptions = []; + this._bInitialized = true; + this.startBatch(); + + var oMsg = $.extend(msgHandshake, {id: String(this._nNextId++)}); + + this._oTransport._send(this._sUrl, oMsg, $.comet._finishInit); + }; + + this._finishInit = function(sReturn) + { + var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')[0]) : sReturn[0]; + + if(oReturn.advice) + $.comet._advice = oReturn.advice; + + var bSuccess = (oReturn.successful) ? oReturn.successful : false; + // do version check + + if(bSuccess) + { + // pick transport ? + // ...... + + $.comet._oTransport._comet = $.comet; + $.comet._oTransport.version = $.comet.version; + + $.comet.clientId = oReturn.clientId; + $.comet._oTransport.startup(oReturn); + $.comet.endBatch(); + } + }; + + this._sendMessage = function(oMsg) + { + if($.comet._nBatch <= 0) + { + if(oMsg.length > 0) + for(var i in oMsg) + { + oMsg[i].clientId = String($.comet.clientId); + oMsg[i].id = String($.comet._nNextId++); + } + else + { + oMsg.clientId = String($.comet.clientId); + oMsg.id = String($.comet._nNextId++); + } + + $.comet._oTransport._send($.comet._sUrl, oMsg); + } + else + { + $.comet._aMessageQueue.push(oMsg); + } + }; + + + this.startBatch = function() { this._nBatch++ }; + this.endBatch = function() { + if(--this._nBatch <= 0) + { + this._nBatch = 0; + if(this._aMessageQueue.length > 0) + { + this._sendMessage(this._aMessageQueue); + this._aMessageQueue = []; + } + } + }; + + this.subscribe = function(sSubscription, fCallback) + { + // if this topic has not been subscribed to yet, send the message now + if(!this._aSubscriptions[sSubscription]) + { + this._aSubscriptions.push(sSubscription) + + if (fCallback) { + this._aSubscriptionCallbacks[sSubscription] = fCallback; + } + + this._sendMessage({ channel: '/meta/subscribe', subscription: sSubscription }); + } + + //$.event.add(window, sSubscription, fCallback); + }; + + this.unsubscribe = function(sSubscription) { + $.comet._sendMessage({ channel: '/meta/unsubscribe', subscription: sSubscription }); + }; + + this.publish = function(sChannel, oData) + { + $.comet._sendMessage({channel: sChannel, data: oData}); + }; + + this.deliver = function(sReturn) + { + var oReturn = sReturn;//eval(sReturn); + + $(oReturn).each(function() + { + $.comet._deliver(this); + }); + }; + + this.disconnect = function() + { + $($.comet._aSubscriptions).each(function(i) + { + $.comet.unsubscribe($.comet._aSubscriptions[i]); + }); + + $.comet._sendMessage({channel:'/meta/disconnect'}); + + $.comet._bInitialized = false; + } + + this._deliver = function(oMsg,oData) + { + if(oMsg.advice) + { + $.comet._advice = oMsg.advice; + } + + switch(oMsg.channel) + { + case '/meta/connect': + if(oMsg.successful && !$.comet._bConnected) + { + $.comet._bConnected = $.comet._bInitialized; + $.comet.endBatch(); + /* + $.comet._sendMessage(msgConnect); + */ + } + else + {} + //$.comet._bConnected = false; + break; + + // add in subscription handling stuff + case '/meta/subscribe': + if(!oMsg.successful) + { + $.comet._oTransport._cancelConnect(); + return; + } + break; + + case '/meta/unsubscribe': + if(!oMsg.successful) + { + $.comet._oTransport._cancelConnect(); + return; + } + break; + + } + + if(oMsg.data) + { + if($.comet._bTrigger) + { + $.event.trigger(oMsg.channel, [oMsg]); + } + + var cb = $.comet._aSubscriptionCallbacks[oMsg.channel]; + if (cb) { + cb(oMsg); + } + } + }; +}; + +})(jQuery); diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js new file mode 100644 index 0000000000..f4da1f47cd --- /dev/null +++ b/plugins/Comet/updatetimeline.js @@ -0,0 +1,3 @@ +// update the local timeline from a Comet server +// + From 056d0a2555bb6783a2bb4632d2c6ad9f52dde5ec Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 25 Apr 2009 14:20:57 -0400 Subject: [PATCH 29/83] remove unused duplicate file --- plugins/Comet/bayeux.class.inc.phps | 123 ---------------------------- 1 file changed, 123 deletions(-) delete mode 100644 plugins/Comet/bayeux.class.inc.phps diff --git a/plugins/Comet/bayeux.class.inc.phps b/plugins/Comet/bayeux.class.inc.phps deleted file mode 100644 index ea004a4532..0000000000 --- a/plugins/Comet/bayeux.class.inc.phps +++ /dev/null @@ -1,123 +0,0 @@ - http://morglog.alleycatracing.com - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -class Bayeux - { - private $oCurl = ''; - private $nNextId = 0; - - public $sUrl = ''; - - function __construct($sUrl) - { - $this->sUrl = $sUrl; - - $this->oCurl = curl_init(); - - $aHeaders = array(); - $aHeaders[] = 'Connection: Keep-Alive'; - - curl_setopt($this->oCurl, CURLOPT_URL, $sUrl); - curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders); - curl_setopt($this->oCurl, CURLOPT_HEADER, 0); - curl_setopt($this->oCurl, CURLOPT_POST, 1); - curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); - - $this->handShake(); - } - - function __destruct() - { - $this->disconnect(); - } - - function handShake() - { - $msgHandshake = array(); - $msgHandshake['channel'] = '/meta/handshake'; - $msgHandshake['version'] = "1.0"; - $msgHandshake['minimumVersion'] = "0.9"; - $msgHandshake['id'] = $this->nNextId++; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); - - $data = curl_exec($this->oCurl); - - if(curl_errno($this->oCurl)) - die("Error: " . curl_error($this->oCurl)); - - $oReturn = json_decode($data); - $oReturn = $oReturn[0]; - - $bSuccessful = ($oReturn->successful) ? true : false; - - if($bSuccessful) - { - $this->clientId = $oReturn->clientId; - - $this->connect(); - } - } - - public function connect() - { - $aMsg['channel'] = '/meta/connect'; - $aMsg['id'] = $this->nNextId++; - $aMsg['clientId'] = $this->clientId; - $aMsg['connectionType'] = 'long-polling'; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); - - $data = curl_exec($this->oCurl); - } - - function disconnect() - { - $msgHandshake = array(); - $msgHandshake['channel'] = '/meta/disconnect'; - $msgHandshake['id'] = $this->nNextId++; - $msgHandshake['clientId'] = $this->clientId; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); - - curl_exec($this->oCurl); - } - - public function publish($sChannel, $oData) - { - if(!$sChannel || !$oData) - return; - - $aMsg = array(); - - $aMsg['channel'] = $sChannel; - $aMsg['id'] = $this->nNextId++; - $aMsg['data'] = $oData; - $aMsg['clientId'] = $this->clientId; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); - - $data = curl_exec($this->oCurl); -// var_dump($data); - } - } From 262dbeac787ad3aecb28c470484eb3fc8d036d93 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 12:06:50 -0400 Subject: [PATCH 30/83] Some updates for testing Comet --- plugins/Comet/bayeux.class.inc.php | 2 - plugins/Comet/jquery.comet.js | 1814 ++++++++++++++++++++++------ plugins/Comet/updatetimeline.js | 27 + 3 files changed, 1478 insertions(+), 365 deletions(-) diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php index 602a7b6446..785d3e3935 100644 --- a/plugins/Comet/bayeux.class.inc.php +++ b/plugins/Comet/bayeux.class.inc.php @@ -69,8 +69,6 @@ class Bayeux $oReturn = json_decode($data); - common_debug(print_r($oReturn, true)); - if (is_array($oReturn)) { $oReturn = $oReturn[0]; } diff --git a/plugins/Comet/jquery.comet.js b/plugins/Comet/jquery.comet.js index 2124e882cb..6de437fa8e 100644 --- a/plugins/Comet/jquery.comet.js +++ b/plugins/Comet/jquery.comet.js @@ -1,363 +1,1451 @@ -(function($) -{ - var msgHandshake = - { - version: '1.0', - minimumVersion: '0.9', - channel: '/meta/handshake' - }; - - var oTransport = function() - { - this._bXD = - (($.comet._sUrl.substring(0,4) == 'http') && ($.comet._sUrl.substr(7,location.href.length).replace(/\/.*/, '') != location.host)) - ? - true - :false; - - this.connectionType = (this._bXD) ? 'callback-polling' : 'long-polling'; - - this.startup = function(oReturn) - { - if(this._comet._bConnected) return; - this.tunnelInit(); - }; - - this.tunnelInit = function() - { - var msgConnect = - { - channel: '/meta/connect', - clientId: $.comet.clientId, - id: String($.comet._nNextId++), - connectionType: $.comet._oTransport.connectionType - }; - - this.openTunnel(msgConnect); - }; - - this.openTunnel = function(oMsg) - { - $.comet._bPolling = true; - - this._send($.comet._sUrl, oMsg, function(sReturn) - { - var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')) : sReturn; - $.comet._bPolling = false; - $.comet.deliver(oReturn); - $.comet._oTransport.closeTunnel(); - }); - }; - - this.closeTunnel = function() - { - if(!$.comet._bInitialized) return; - - if($.comet._advice) - { - if($.comet._advice.reconnect == 'none') return; - - if($.comet._advice.interval > 0) - { - setTimeout($.comet._oTransport._connect, $.comet._advice.interval); - } - else - { - $.comet._oTransport._connect(); - } - } - else - { - $.comet._oTransport._connect(); - } - }; - - this._connect = function() - { - if(!$.comet._bInitialized) return; - - if($.comet._bPolling) return; - - if($.comet._advice && $.comet._advice.reconnect == 'handshake') - { - $.comet._bConnected = false; - $.comet.init($.comet._sUrl); - } - else if($.comet._bConnected) - { - var msgConnect = - { - //jsonp: 'test', - clientId: $.comet.clientId, - id: String($.comet._nNextId++), - channel: '/meta/connect', - connectionType: $.comet._oTransport.connectionType - }; - $.comet._oTransport.openTunnel(msgConnect); - } - }; - - this._send = function(sUrl, oMsg, fCallback) { - //default callback will check advice, deliver messages, and reconnect - var fCallback = (fCallback) ? fCallback : function(sReturn) - { - var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')) : sReturn; - - $.comet.deliver(oReturn); - - if($.comet._advice) - { - if($.comet._advice.reconnect == 'none') - return; - - if($.comet._advice.interval > 0) - { - setTimeout($.comet._oTransport._connect, $.comet._advice.interval); - } - else - { - $.comet._oTransport._connect(); - } - } - else - { - $.comet._oTransport._connect(); - } - }; - - //regular AJAX for same domain calls - if((!this._bXD) && (this.connectionType == 'long-polling')) - { - this._pollRequest = $.ajax({ - url: sUrl, - type: 'post', - beforeSend: function(oXhr) { oXhr.setRequestHeader('Connection', 'Keep-Alive'); }, - data: { message: JSON.stringify(oMsg) }, - success: fCallback - }); - } - else // JSONP callback for cross domain - { - this._pollRequest = $.ajax({ - url: sUrl, - dataType: 'jsonp', - jsonp: 'jsonp', - beforeSend: function(oXhr) { oXhr.setRequestHeader('Connection', 'Keep-Alive'); }, - data: - { - message: JSON.stringify($.extend(oMsg,{connectionType: 'callback-polling' })) - }, - success: fCallback - }); - } - } - }; - - $.comet = new function() - { - this.CONNECTED = 'CONNECTED'; - this.CONNECTING = 'CONNECTING'; - this.DISCONNECTED = 'DISCONNECTED'; - this.DISCONNECTING = 'DISCONNECTING'; - - this._aMessageQueue = []; - this._aSubscriptions = []; - this._aSubscriptionCallbacks = []; - this._bInitialized = false; - this._bConnected = false; - this._nBatch = 0; - this._nNextId = 0; - // just define the transport, do not assign it yet. - this._oTransport = ''; //oTransport; - this._sUrl = ''; - - this.supportedConectionTypes = [ 'long-polling', 'callback-polling' ]; - - this.clientId = ''; - - this._bTrigger = true; // this sends $.event.trigger(channel, data) - - this.init = function(sUrl) - { - this._sUrl = (sUrl) ? sUrl : '/cometd'; - - this._oTransport = new oTransport(); - - this._aMessageQueue = []; - this._aSubscriptions = []; - this._bInitialized = true; - this.startBatch(); - - var oMsg = $.extend(msgHandshake, {id: String(this._nNextId++)}); - - this._oTransport._send(this._sUrl, oMsg, $.comet._finishInit); - }; - - this._finishInit = function(sReturn) - { - var oReturn = (typeof sReturn != "object") ? (eval('(' + sReturn + ')')[0]) : sReturn[0]; - - if(oReturn.advice) - $.comet._advice = oReturn.advice; - - var bSuccess = (oReturn.successful) ? oReturn.successful : false; - // do version check - - if(bSuccess) - { - // pick transport ? - // ...... - - $.comet._oTransport._comet = $.comet; - $.comet._oTransport.version = $.comet.version; - - $.comet.clientId = oReturn.clientId; - $.comet._oTransport.startup(oReturn); - $.comet.endBatch(); - } - }; - - this._sendMessage = function(oMsg) - { - if($.comet._nBatch <= 0) - { - if(oMsg.length > 0) - for(var i in oMsg) - { - oMsg[i].clientId = String($.comet.clientId); - oMsg[i].id = String($.comet._nNextId++); - } - else - { - oMsg.clientId = String($.comet.clientId); - oMsg.id = String($.comet._nNextId++); - } - - $.comet._oTransport._send($.comet._sUrl, oMsg); - } - else - { - $.comet._aMessageQueue.push(oMsg); - } - }; - - - this.startBatch = function() { this._nBatch++ }; - this.endBatch = function() { - if(--this._nBatch <= 0) - { - this._nBatch = 0; - if(this._aMessageQueue.length > 0) - { - this._sendMessage(this._aMessageQueue); - this._aMessageQueue = []; - } - } - }; - - this.subscribe = function(sSubscription, fCallback) - { - // if this topic has not been subscribed to yet, send the message now - if(!this._aSubscriptions[sSubscription]) - { - this._aSubscriptions.push(sSubscription) - - if (fCallback) { - this._aSubscriptionCallbacks[sSubscription] = fCallback; - } - - this._sendMessage({ channel: '/meta/subscribe', subscription: sSubscription }); - } - - //$.event.add(window, sSubscription, fCallback); - }; - - this.unsubscribe = function(sSubscription) { - $.comet._sendMessage({ channel: '/meta/unsubscribe', subscription: sSubscription }); - }; - - this.publish = function(sChannel, oData) - { - $.comet._sendMessage({channel: sChannel, data: oData}); - }; - - this.deliver = function(sReturn) - { - var oReturn = sReturn;//eval(sReturn); - - $(oReturn).each(function() - { - $.comet._deliver(this); - }); - }; - - this.disconnect = function() - { - $($.comet._aSubscriptions).each(function(i) - { - $.comet.unsubscribe($.comet._aSubscriptions[i]); - }); - - $.comet._sendMessage({channel:'/meta/disconnect'}); - - $.comet._bInitialized = false; - } - - this._deliver = function(oMsg,oData) - { - if(oMsg.advice) - { - $.comet._advice = oMsg.advice; - } - - switch(oMsg.channel) - { - case '/meta/connect': - if(oMsg.successful && !$.comet._bConnected) - { - $.comet._bConnected = $.comet._bInitialized; - $.comet.endBatch(); - /* - $.comet._sendMessage(msgConnect); - */ - } - else - {} - //$.comet._bConnected = false; - break; - - // add in subscription handling stuff - case '/meta/subscribe': - if(!oMsg.successful) - { - $.comet._oTransport._cancelConnect(); - return; - } - break; - - case '/meta/unsubscribe': - if(!oMsg.successful) - { - $.comet._oTransport._cancelConnect(); - return; - } - break; - - } - - if(oMsg.data) - { - if($.comet._bTrigger) - { - $.event.trigger(oMsg.channel, [oMsg]); - } - - var cb = $.comet._aSubscriptionCallbacks[oMsg.channel]; - if (cb) { - cb(oMsg); - } - } - }; -}; - -})(jQuery); +/** + * Copyright 2008 Mort Bay Consulting Pty. Ltd. + * Dual licensed under the Apache License 2.0 and the MIT license. + * ---------------------------------------------------------------------------- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http: *www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * Licensed under the MIT license; + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + * $Revision$ $Date$ + */ +(function($) +{ + /** + * The constructor for a Comet object. + * There is a default Comet instance already created at the variable $.cometd, + * and hence that can be used to start a comet conversation with a server. + * In the rare case a page needs more than one comet conversation, a new instance can be + * created via: + *
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * 
+ */ + $.Cometd = function(name) + { + var _name = name || 'default'; + var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 }; + var _logLevel = 'info'; + var _url; + var _xd = false; + var _transport; + var _status = 'disconnected'; + var _messageId = 0; + var _clientId = null; + var _batch = 0; + var _messageQueue = []; + var _listeners = {}; + var _backoff = 0; + var _backoffIncrement = 1000; + var _maxBackoff = 60000; + var _scheduledSend = null; + var _extensions = []; + var _advice = {}; + var _handshakeProps; + + /** + * Returns the name assigned to this Comet object, or the string 'default' + * if no name has been explicitely passed as parameter to the constructor. + */ + this.getName = function() + { + return _name; + }; + + /** + * Configures the initial comet communication with the comet server. + * @param cometURL the URL of the comet server + */ + this.configure = function(cometURL) + { + _configure(cometURL); + }; + + function _configure(cometURL) + { + _url = cometURL; + _debug('Initializing comet with url: {}', _url); + + // Check immediately if we're cross domain + // If cross domain, the handshake must not send the long polling transport type + var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL); + if (urlParts[3]) _xd = urlParts[3] != location.host; + + // Temporary setup a transport to send the initial handshake + // The transport may be changed as a result of handshake + if (_xd) + _transport = newCallbackPollingTransport(); + else + _transport = newLongPollingTransport(); + _debug('Initial transport is {}', _transport.getType()); + }; + + /** + * Configures and establishes the comet communication with the comet server + * via a handshake and a subsequent connect. + * @param cometURL the URL of the comet server + * @param handshakeProps an object to be merged with the handshake message + * @see #configure(cometURL) + * @see #handshake(handshakeProps) + */ + this.init = function(cometURL, handshakeProps) + { + _configure(cometURL); + _handshake(handshakeProps); + }; + + /** + * Establishes the comet communication with the comet server + * via a handshake and a subsequent connect. + * @param handshakeProps an object to be merged with the handshake message + */ + this.handshake = function(handshakeProps) + { + _handshake(handshakeProps); + }; + + /** + * Disconnects from the comet server. + * @param disconnectProps an object to be merged with the disconnect message + */ + this.disconnect = function(disconnectProps) + { + var bayeuxMessage = { + channel: '/meta/disconnect' + }; + var message = $.extend({}, disconnectProps, bayeuxMessage); + // Deliver immediately + // The handshake and connect mechanism make use of startBatch(), and in case + // of a failed handshake the disconnect would not be delivered if using _send(). + _setStatus('disconnecting'); + _deliver([message], false); + }; + + /** + * Marks the start of a batch of application messages to be sent to the server + * in a single request, obtaining a single response containing (possibly) many + * application reply messages. + * Messages are held in a queue and not sent until {@link #endBatch()} is called. + * If startBatch() is called multiple times, then an equal number of endBatch() + * calls must be made to close and send the batch of messages. + * @see #endBatch() + */ + this.startBatch = function() + { + _startBatch(); + }; + + /** + * Marks the end of a batch of application messages to be sent to the server + * in a single request. + * @see #startBatch() + */ + this.endBatch = function() + { + _endBatch(true); + }; + + /** + * Subscribes to the given channel, performing the given callback in the given scope + * when a message for the channel arrives. + * @param channel the channel to subscribe to + * @param scope the scope of the callback + * @param callback the callback to call when a message is delivered to the channel + * @param subscribeProps an object to be merged with the subscribe message + * @return the subscription handle to be passed to {@link #unsubscribe(object)} + */ + this.subscribe = function(channel, scope, callback, subscribeProps) + { + var subscription = this.addListener(channel, scope, callback); + + // Send the subscription message after the subscription registration to avoid + // races where the server would deliver a message to the subscribers, but here + // on the client the subscription has not been added yet to the data structures + var bayeuxMessage = { + channel: '/meta/subscribe', + subscription: channel + }; + var message = $.extend({}, subscribeProps, bayeuxMessage); + _send(message); + + return subscription; + }; + + /** + * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}. + * @param subscription the subscription to unsubscribe. + */ + this.unsubscribe = function(subscription, unsubscribeProps) + { + // Remove the local listener before sending the message + // This ensures that if the server fails, this client does not get notifications + this.removeListener(subscription); + var bayeuxMessage = { + channel: '/meta/unsubscribe', + subscription: subscription[0] + }; + var message = $.extend({}, unsubscribeProps, bayeuxMessage); + _send(message); + }; + + /** + * Publishes a message on the given channel, containing the given content. + * @param channel the channel to publish the message to + * @param content the content of the message + * @param publishProps an object to be merged with the publish message + */ + this.publish = function(channel, content, publishProps) + { + var bayeuxMessage = { + channel: channel, + data: content + }; + var message = $.extend({}, publishProps, bayeuxMessage); + _send(message); + }; + + /** + * Adds a listener for bayeux messages, performing the given callback in the given scope + * when a message for the given channel arrives. + * @param channel the channel the listener is interested to + * @param scope the scope of the callback + * @param callback the callback to call when a message is delivered to the channel + * @returns the subscription handle to be passed to {@link #removeListener(object)} + * @see #removeListener(object) + */ + this.addListener = function(channel, scope, callback) + { + // The data structure is a map, where each subscription + // holds the callback to be called and its scope. + + // Normalize arguments + if (!callback) + { + callback = scope; + scope = undefined; + } + + var subscription = { + scope: scope, + callback: callback + }; + + var subscriptions = _listeners[channel]; + if (!subscriptions) + { + subscriptions = []; + _listeners[channel] = subscriptions; + } + // Pushing onto an array appends at the end and returns the id associated with the element increased by 1. + // Note that if: + // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c'); + // then: + // hc==3, a.join()=='a',,'c', a.length==3 + var subscriptionIndex = subscriptions.push(subscription) - 1; + _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex); + + // The subscription to allow removal of the listener is made of the channel and the index + return [channel, subscriptionIndex]; + }; + + /** + * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}. + * @param subscription the subscription to unsubscribe. + */ + this.removeListener = function(subscription) + { + var subscriptions = _listeners[subscription[0]]; + if (subscriptions) + { + delete subscriptions[subscription[1]]; + _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]); + } + }; + + /** + * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or + * {@link #subscribe(channel, scope, callback)}. + */ + this.clearListeners = function() + { + _listeners = {}; + }; + + /** + * Returns a string representing the status of the bayeux communication with the comet server. + */ + this.getStatus = function() + { + return _status; + }; + + /** + * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. + * Default value is 1 second, which means if there is a persistent failure the retries will happen + * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of + * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed). + * @param period the backoff period to set + * @see #getBackoffIncrement() + */ + this.setBackoffIncrement = function(period) + { + _backoffIncrement = period; + }; + + /** + * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. + * @see #setBackoffIncrement(period) + */ + this.getBackoffIncrement = function() + { + return _backoffIncrement; + }; + + /** + * Returns the backoff period to wait before retrying an unsuccessful or failed message. + */ + this.getBackoffPeriod = function() + { + return _backoff; + }; + + /** + * Sets the log level for console logging. + * Valid values are the strings 'error', 'warn', 'info' and 'debug', from + * less verbose to more verbose. + * @param level the log level string + */ + this.setLogLevel = function(level) + { + _logLevel = level; + }; + + /** + * Registers an extension whose callbacks are called for every incoming message + * (that comes from the server to this client implementation) and for every + * outgoing message (that originates from this client implementation for the + * server). + * The format of the extension object is the following: + *
+         * {
+         *     incoming: function(message) { ... },
+         *     outgoing: function(message) { ... }
+         * }
+         * Both properties are optional, but if they are present they will be called
+         * respectively for each incoming message and for each outgoing message.
+         * 
+ * @param name the name of the extension + * @param extension the extension to register + * @return true if the extension was registered, false otherwise + * @see #unregisterExtension(name) + */ + this.registerExtension = function(name, extension) + { + var existing = false; + for (var i = 0; i < _extensions.length; ++i) + { + var existingExtension = _extensions[i]; + if (existingExtension.name == name) + { + existing = true; + return false; + } + } + if (!existing) + { + _extensions.push({ + name: name, + extension: extension + }); + _debug('Registered extension \'{}\'', name); + return true; + } + else + { + _info('Could not register extension with name \'{}\': another extension with the same name already exists'); + return false; + } + }; + + /** + * Unregister an extension previously registered with + * {@link #registerExtension(name, extension)}. + * @param name the name of the extension to unregister. + * @return true if the extension was unregistered, false otherwise + */ + this.unregisterExtension = function(name) + { + var unregistered = false; + $.each(_extensions, function(index, extension) + { + if (extension.name == name) + { + _extensions.splice(index, 1); + unregistered = true; + _debug('Unregistered extension \'{}\'', name); + return false; + } + }); + return unregistered; + }; + + /** + * Starts a the batch of messages to be sent in a single request. + * @see _endBatch(deliverMessages) + */ + function _startBatch() + { + ++_batch; + }; + + /** + * Ends the batch of messages to be sent in a single request, + * optionally delivering messages present in the message queue depending + * on the given argument. + * @param deliverMessages whether to deliver the messages in the queue or not + * @see _startBatch() + */ + function _endBatch(deliverMessages) + { + --_batch; + if (_batch < 0) _batch = 0; + if (deliverMessages && _batch == 0 && !_isDisconnected()) + { + var messages = _messageQueue; + _messageQueue = []; + if (messages.length > 0) _deliver(messages, false); + } + }; + + function _nextMessageId() + { + return ++_messageId; + }; + + /** + * Converts the given response into an array of bayeux messages + * @param response the response to convert + * @return an array of bayeux messages obtained by converting the response + */ + function _convertToMessages(response) + { + if (response === undefined) return []; + if (response instanceof Array) return response; + if (response instanceof String || typeof response == 'string') return eval('(' + response + ')'); + if (response instanceof Object) return [response]; + throw 'Conversion Error ' + response + ', typeof ' + (typeof response); + }; + + function _setStatus(newStatus) + { + _debug('{} -> {}', _status, newStatus); + _status = newStatus; + }; + + function _isDisconnected() + { + return _status == 'disconnecting' || _status == 'disconnected'; + }; + + /** + * Sends the initial handshake message + */ + function _handshake(handshakeProps) + { + _debug('Starting handshake'); + _clientId = null; + + // Start a batch. + // This is needed because handshake and connect are async. + // It may happen that the application calls init() then subscribe() + // and the subscribe message is sent before the connect message, if + // the subscribe message is not held until the connect message is sent. + // So here we start a batch to hold temporarly any message until + // the connection is fully established. + _batch = 0; + _startBatch(); + + // Save the original properties provided by the user + // Deep copy to avoid the user to be able to change them later + _handshakeProps = $.extend(true, {}, handshakeProps); + + var bayeuxMessage = { + version: '1.0', + minimumVersion: '0.9', + channel: '/meta/handshake', + supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling'] + }; + // Do not allow the user to mess with the required properties, + // so merge first the user properties and *then* the bayeux message + var message = $.extend({}, handshakeProps, bayeuxMessage); + + // We started a batch to hold the application messages, + // so here we must bypass it and deliver immediately. + _setStatus('handshaking'); + _deliver([message], false); + }; + + function _findTransport(handshakeResponse) + { + var transportTypes = handshakeResponse.supportedConnectionTypes; + if (_xd) + { + // If we are cross domain, check if the server supports it, that's the only option + if ($.inArray('callback-polling', transportTypes) >= 0) return _transport; + } + else + { + // Check if we can keep long-polling + if ($.inArray('long-polling', transportTypes) >= 0) return _transport; + + // The server does not support long-polling + if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport(); + } + return null; + }; + + function _delayedHandshake() + { + _setStatus('handshaking'); + _delayedSend(function() + { + _handshake(_handshakeProps); + }); + }; + + function _delayedConnect() + { + _setStatus('connecting'); + _delayedSend(function() + { + _connect(); + }); + }; + + function _delayedSend(operation) + { + _cancelDelayedSend(); + var delay = _backoff; + _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval); + if (_advice.interval && _advice.interval > 0) + delay += _advice.interval; + _scheduledSend = _setTimeout(operation, delay); + }; + + function _cancelDelayedSend() + { + if (_scheduledSend !== null) clearTimeout(_scheduledSend); + _scheduledSend = null; + }; + + function _setTimeout(funktion, delay) + { + return setTimeout(function() + { + try + { + funktion(); + } + catch (x) + { + _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x); + } + }, delay); + }; + + /** + * Sends the connect message + */ + function _connect() + { + _debug('Starting connect'); + var message = { + channel: '/meta/connect', + connectionType: _transport.getType() + }; + _setStatus('connecting'); + _deliver([message], true); + _setStatus('connected'); + }; + + function _send(message) + { + if (_batch > 0) + _messageQueue.push(message); + else + _deliver([message], false); + }; + + /** + * Delivers the messages to the comet server + * @param messages the array of messages to send + */ + function _deliver(messages, comet) + { + // We must be sure that the messages have a clientId. + // This is not guaranteed since the handshake may take time to return + // (and hence the clientId is not known yet) and the application + // may create other messages. + $.each(messages, function(index, message) + { + message['id'] = _nextMessageId(); + if (_clientId) message['clientId'] = _clientId; + messages[index] = _applyOutgoingExtensions(message); + }); + + var self = this; + var envelope = { + url: _url, + messages: messages, + onSuccess: function(request, response) + { + try + { + _handleSuccess.call(self, request, response, comet); + } + catch (x) + { + _debug('Exception during execution of success callback: {}', x); + } + }, + onFailure: function(request, reason, exception) + { + try + { + _handleFailure.call(self, request, messages, reason, exception, comet); + } + catch (x) + { + _debug('Exception during execution of failure callback: {}', x); + } + } + }; + _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages)); + _transport.send(envelope, comet); + }; + + function _applyIncomingExtensions(message) + { + for (var i = 0; i < _extensions.length; ++i) + { + var extension = _extensions[i]; + var callback = extension.extension.incoming; + if (callback && typeof callback === 'function') + { + _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name); + message = _applyExtension(extension.name, callback, message) || message; + } + } + return message; + }; + + function _applyOutgoingExtensions(message) + { + for (var i = 0; i < _extensions.length; ++i) + { + var extension = _extensions[i]; + var callback = extension.extension.outgoing; + if (callback && typeof callback === 'function') + { + _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name); + message = _applyExtension(extension.name, callback, message) || message; + } + } + return message; + }; + + function _applyExtension(name, callback, message) + { + try + { + return callback(message); + } + catch (x) + { + _debug('Exception during execution of extension \'{}\': {}', name, x); + return message; + } + }; + + function _handleSuccess(request, response, comet) + { + var messages = _convertToMessages(response); + _debug('Received response {}', JSON.stringify(messages)); + + // Signal the transport it can deliver other queued requests + _transport.complete(request, true, comet); + + for (var i = 0; i < messages.length; ++i) + { + var message = messages[i]; + message = _applyIncomingExtensions(message); + + if (message.advice) _advice = message.advice; + + var channel = message.channel; + switch (channel) + { + case '/meta/handshake': + _handshakeSuccess(message); + break; + case '/meta/connect': + _connectSuccess(message); + break; + case '/meta/disconnect': + _disconnectSuccess(message); + break; + case '/meta/subscribe': + _subscribeSuccess(message); + break; + case '/meta/unsubscribe': + _unsubscribeSuccess(message); + break; + default: + _messageSuccess(message); + break; + } + } + }; + + function _handleFailure(request, messages, reason, exception, comet) + { + var xhr = request.xhr; + _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception); + + // Signal the transport it can deliver other queued requests + _transport.complete(request, false, comet); + + for (var i = 0; i < messages.length; ++i) + { + var message = messages[i]; + var channel = message.channel; + switch (channel) + { + case '/meta/handshake': + _handshakeFailure(xhr, message); + break; + case '/meta/connect': + _connectFailure(xhr, message); + break; + case '/meta/disconnect': + _disconnectFailure(xhr, message); + break; + case '/meta/subscribe': + _subscribeFailure(xhr, message); + break; + case '/meta/unsubscribe': + _unsubscribeFailure(xhr, message); + break; + default: + _messageFailure(xhr, message); + break; + } + } + }; + + function _handshakeSuccess(message) + { + if (message.successful) + { + _debug('Handshake successful'); + // Save clientId, figure out transport, then follow the advice to connect + _clientId = message.clientId; + + var newTransport = _findTransport(message); + if (newTransport === null) + { + throw 'Could not agree on transport with server'; + } + else + { + if (_transport.getType() != newTransport.getType()) + { + _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType()); + _transport = newTransport; + } + } + + // Notify the listeners + // Here the new transport is in place, as well as the clientId, so + // the listener can perform a publish() if it wants, and the listeners + // are notified before the connect below. + _notifyListeners('/meta/handshake', message); + + var action = _advice.reconnect ? _advice.reconnect : 'retry'; + switch (action) + { + case 'retry': + _delayedConnect(); + break; + default: + break; + } + } + else + { + _debug('Handshake unsuccessful'); + + var retry = !_isDisconnected() && _advice.reconnect != 'none'; + if (!retry) _setStatus('disconnected'); + + _notifyListeners('/meta/handshake', message); + _notifyListeners('/meta/unsuccessful', message); + + // Only try again if we haven't been disconnected and + // the advice permits us to retry the handshake + if (retry) + { + _increaseBackoff(); + _debug('Handshake failure, backing off and retrying in {} ms', _backoff); + _delayedHandshake(); + } + } + }; + + function _handshakeFailure(xhr, message) + { + _debug('Handshake failure'); + + // Notify listeners + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/handshake', + request: message, + xhr: xhr, + advice: { + action: 'retry', + interval: _backoff + } + }; + + var retry = !_isDisconnected() && _advice.reconnect != 'none'; + if (!retry) _setStatus('disconnected'); + + _notifyListeners('/meta/handshake', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + + // Only try again if we haven't been disconnected and the + // advice permits us to try again + if (retry) + { + _increaseBackoff(); + _debug('Handshake failure, backing off and retrying in {} ms', _backoff); + _delayedHandshake(); + } + }; + + function _connectSuccess(message) + { + var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry'); + if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting'); + + if (message.successful) + { + _debug('Connect successful'); + + // End the batch and allow held messages from the application + // to go to the server (see _handshake() where we start the batch). + // The batch is ended before notifying the listeners, so that + // listeners can batch other cometd operations + _endBatch(true); + + // Notify the listeners after the status change but before the next connect + _notifyListeners('/meta/connect', message); + + // Connect was successful. + // Normally, the advice will say "reconnect: 'retry', interval: 0" + // and the server will hold the request, so when a response returns + // we immediately call the server again (long polling) + switch (action) + { + case 'retry': + _resetBackoff(); + _delayedConnect(); + break; + default: + _resetBackoff(); + _setStatus('disconnected'); + break; + } + } + else + { + _debug('Connect unsuccessful'); + + // Notify the listeners after the status change but before the next action + _notifyListeners('/meta/connect', message); + _notifyListeners('/meta/unsuccessful', message); + + // Connect was not successful. + // This may happen when the server crashed, the current clientId + // will be invalid, and the server will ask to handshake again + switch (action) + { + case 'retry': + _increaseBackoff(); + _delayedConnect(); + break; + case 'handshake': + // End the batch but do not deliver the messages until we connect successfully + _endBatch(false); + _resetBackoff(); + _delayedHandshake(); + break; + case 'none': + _resetBackoff(); + _setStatus('disconnected'); + break; + } + } + }; + + function _connectFailure(xhr, message) + { + _debug('Connect failure'); + + // Notify listeners + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/connect', + request: message, + xhr: xhr, + advice: { + action: 'retry', + interval: _backoff + } + }; + _notifyListeners('/meta/connect', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + + if (!_isDisconnected()) + { + var action = _advice.reconnect ? _advice.reconnect : 'retry'; + switch (action) + { + case 'retry': + _increaseBackoff(); + _debug('Connect failure, backing off and retrying in {} ms', _backoff); + _delayedConnect(); + break; + case 'handshake': + _resetBackoff(); + _delayedHandshake(); + break; + case 'none': + _resetBackoff(); + break; + default: + _debug('Unrecognized reconnect value: {}', action); + break; + } + } + }; + + function _disconnectSuccess(message) + { + if (message.successful) + { + _debug('Disconnect successful'); + _disconnect(false); + _notifyListeners('/meta/disconnect', message); + } + else + { + _debug('Disconnect unsuccessful'); + _disconnect(true); + _notifyListeners('/meta/disconnect', message); + _notifyListeners('/meta/usuccessful', message); + } + }; + + function _disconnect(abort) + { + _cancelDelayedSend(); + if (abort) _transport.abort(); + _clientId = null; + _setStatus('disconnected'); + _batch = 0; + _messageQueue = []; + _resetBackoff(); + }; + + function _disconnectFailure(xhr, message) + { + _debug('Disconnect failure'); + _disconnect(true); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/disconnect', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/disconnect', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _subscribeSuccess(message) + { + if (message.successful) + { + _debug('Subscribe successful'); + _notifyListeners('/meta/subscribe', message); + } + else + { + _debug('Subscribe unsuccessful'); + _notifyListeners('/meta/subscribe', message); + _notifyListeners('/meta/unsuccessful', message); + } + }; + + function _subscribeFailure(xhr, message) + { + _debug('Subscribe failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/subscribe', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/subscribe', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _unsubscribeSuccess(message) + { + if (message.successful) + { + _debug('Unsubscribe successful'); + _notifyListeners('/meta/unsubscribe', message); + } + else + { + _debug('Unsubscribe unsuccessful'); + _notifyListeners('/meta/unsubscribe', message); + _notifyListeners('/meta/unsuccessful', message); + } + }; + + function _unsubscribeFailure(xhr, message) + { + _debug('Unsubscribe failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/unsubscribe', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/unsubscribe', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _messageSuccess(message) + { + if (message.successful === undefined) + { + if (message.data) + { + // It is a plain message, and not a bayeux meta message + _notifyListeners(message.channel, message); + } + else + { + _debug('Unknown message {}', JSON.stringify(message)); + } + } + else + { + if (message.successful) + { + _debug('Publish successful'); + _notifyListeners('/meta/publish', message); + } + else + { + _debug('Publish unsuccessful'); + _notifyListeners('/meta/publish', message); + _notifyListeners('/meta/unsuccessful', message); + } + } + }; + + function _messageFailure(xhr, message) + { + _debug('Publish failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: message.channel, + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/publish', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _notifyListeners(channel, message) + { + // Notify direct listeners + _notify(channel, message); + + // Notify the globbing listeners + var channelParts = channel.split("/"); + var last = channelParts.length - 1; + for (var i = last; i > 0; --i) + { + var channelPart = channelParts.slice(0, i).join('/') + '/*'; + // We don't want to notify /foo/* if the channel is /foo/bar/baz, + // so we stop at the first non recursive globbing + if (i == last) _notify(channelPart, message); + // Add the recursive globber and notify + channelPart += '*'; + _notify(channelPart, message); + } + }; + + function _notify(channel, message) + { + var subscriptions = _listeners[channel]; + if (subscriptions && subscriptions.length > 0) + { + for (var i = 0; i < subscriptions.length; ++i) + { + var subscription = subscriptions[i]; + // Subscriptions may come and go, so the array may have 'holes' + if (subscription) + { + try + { + _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name); + subscription.callback.call(subscription.scope, message); + } + catch (x) + { + // Ignore exceptions from callbacks + _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x); + } + } + } + } + }; + + function _resetBackoff() + { + _backoff = 0; + }; + + function _increaseBackoff() + { + if (_backoff < _maxBackoff) _backoff += _backoffIncrement; + }; + + var _error = this._error = function(text, args) + { + _log('error', _format.apply(this, arguments)); + }; + + var _warn = this._warn = function(text, args) + { + _log('warn', _format.apply(this, arguments)); + }; + + var _info = this._info = function(text, args) + { + _log('info', _format.apply(this, arguments)); + }; + + var _debug = this._debug = function(text, args) + { + _log('debug', _format.apply(this, arguments)); + }; + + function _log(level, text) + { + var priority = _logPriorities[level]; + var configPriority = _logPriorities[_logLevel]; + if (!configPriority) configPriority = _logPriorities['info']; + if (priority >= configPriority) + { + if (window.console) window.console.log(text); + } + }; + + function _format(text) + { + var braces = /\{\}/g; + var result = ''; + var start = 0; + var count = 0; + while (braces.test(text)) + { + result += text.substr(start, braces.lastIndex - start - 2); + var arg = arguments[++count]; + result += arg !== undefined ? arg : '{}'; + start = braces.lastIndex; + } + result += text.substr(start, text.length - start); + return result; + }; + + function newLongPollingTransport() + { + return $.extend({}, new Transport('long-polling'), new LongPollingTransport()); + }; + + function newCallbackPollingTransport() + { + return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport()); + }; + + /** + * Base object with the common functionality for transports. + * The key responsibility is to allow at most 2 outstanding requests to the server, + * to avoid that requests are sent behind a long poll. + * To achieve this, we have one reserved request for the long poll, and all other + * requests are serialized one after the other. + */ + var Transport = function(type) + { + var _maxRequests = 2; + var _requestIds = 0; + var _cometRequest = null; + var _requests = []; + var _packets = []; + + this.getType = function() + { + return type; + }; + + this.send = function(packet, comet) + { + if (comet) + _cometSend(this, packet); + else + _send(this, packet); + }; + + function _cometSend(self, packet) + { + if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed'; + + var requestId = ++_requestIds; + _debug('Beginning comet request {}', requestId); + + var request = {id: requestId}; + _debug('Delivering comet request {}', requestId); + self.deliver(packet, request); + _cometRequest = request; + }; + + function _send(self, packet) + { + var requestId = ++_requestIds; + _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); + + var request = {id: requestId}; + // Consider the comet request which should always be present + if (_requests.length < _maxRequests - 1) + { + _debug('Delivering request {}', requestId); + self.deliver(packet, request); + _requests.push(request); + } + else + { + _packets.push([packet, request]); + _debug('Queued request {}, {} queued requests', requestId, _packets.length); + } + }; + + this.complete = function(request, success, comet) + { + if (comet) + _cometComplete(request); + else + _complete(this, request, success); + }; + + function _cometComplete(request) + { + var requestId = request.id; + if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId; + + // Reset comet request + _cometRequest = null; + _debug('Ended comet request {}', requestId); + }; + + function _complete(self, request, success) + { + var requestId = request.id; + var index = $.inArray(request, _requests); + // The index can be negative the request has been aborted + if (index >= 0) _requests.splice(index, 1); + _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); + + if (_packets.length > 0) + { + var packet = _packets.shift(); + if (success) + { + _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length); + _send(self, packet[0]); + } + else + { + _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length); + // Keep the semantic of calling response callbacks asynchronously after the request + setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0); + } + } + }; + + this.abort = function() + { + for (var i = 0; i < _requests.length; ++i) + { + var request = _requests[i]; + _debug('Aborting request {}', request.id); + if (request.xhr) request.xhr.abort(); + } + if (_cometRequest) + { + _debug('Aborting comet request {}', _cometRequest.id); + if (_cometRequest.xhr) _cometRequest.xhr.abort(); + } + _cometRequest = null; + _requests = []; + _packets = []; + }; + }; + + var LongPollingTransport = function() + { + this.deliver = function(packet, request) + { + request.xhr = $.ajax({ + url: packet.url, + type: 'POST', + contentType: 'text/json;charset=UTF-8', + beforeSend: function(xhr) + { + xhr.setRequestHeader('Connection', 'Keep-Alive'); + return true; + }, + data: JSON.stringify(packet.messages), + success: function(response) { packet.onSuccess(request, response); }, + error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } + }); + }; + }; + + var CallbackPollingTransport = function() + { + var _maxLength = 2000; + this.deliver = function(packet, request) + { + // Microsoft Internet Explorer has a 2083 URL max length + // We must ensure that we stay within that length + var messages = JSON.stringify(packet.messages); + // Encode the messages because all brackets, quotes, commas, colons, etc + // present in the JSON will be URL encoded, taking many more characters + var urlLength = packet.url.length + encodeURI(messages).length; + _debug('URL length: {}', urlLength); + // Let's stay on the safe side and use 2000 instead of 2083 + // also because we did not count few characters among which + // the parameter name 'message' and the parameter 'jsonp', + // which sum up to about 50 chars + if (urlLength > _maxLength) + { + var x = packet.messages.length > 1 ? + 'Too many bayeux messages in the same batch resulting in message too big ' + + '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() : + 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' + + 'for transport ' + this.getType(); + // Keep the semantic of calling response callbacks asynchronously after the request + _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0); + } + else + { + $.ajax({ + url: packet.url, + type: 'GET', + dataType: 'jsonp', + jsonp: 'jsonp', + beforeSend: function(xhr) + { + xhr.setRequestHeader('Connection', 'Keep-Alive'); + return true; + }, + data: + { + // In callback-polling, the content must be sent via the 'message' parameter + message: messages + }, + success: function(response) { packet.onSuccess(request, response); }, + error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } + }); + } + }; + }; + }; + + /** + * The JS object that exposes the comet API to applications + */ + $.cometd = new $.Cometd(); // The default instance + +})(jQuery); diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index f4da1f47cd..6612b51168 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -1,3 +1,30 @@ // update the local timeline from a Comet server // +var updater = function() +{ + var _handshook = false; + var _connected = false; + var _cometd; + + return { + init: function() + { + _cometd = $.cometd; // Uses the default Comet object + _cometd.init(_timelineServer); + _cometd.subscribe(_timeline, this, receive); + $(window).unload(leave); + } + } + + function leave() + { + _cometd.disconnect(); + } + + function receive(message) + { + var noticeItem = makeNoticeItem(message.data); + var noticeList = $('ul.notices'); + } +}(); From 84072aa5cf6124d59a06a7f0a7945c00ee2836da Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 12:13:49 -0400 Subject: [PATCH 31/83] run 'set names' after each connection to deal with UTF8 correctly --- classes/Memcached_DataObject.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 5f71f716b3..877bbf2e0f 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -227,4 +227,20 @@ class Memcached_DataObject extends DB_DataObject $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry); return new ArrayWrapper($cached); } + + // We overload so that 'SET NAMES "utf8"' is called for + // each connection + + function _connect() + { + global $_DB_DATAOBJECT; + $exists = !empty($this->_database_dsn_md5) && + isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]); + $result = parent::_connect(); + if (!$exists) { + $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]; + $DB->query('SET NAMES "utf8"'); + } + return $result; + } } From 068f6801cc59488bfc50ef399a2a4d22b1b7e9c2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 12:27:32 -0400 Subject: [PATCH 32/83] Revert "run 'set names' after each connection to deal with UTF8 correctly" This reverts commit 84072aa5cf6124d59a06a7f0a7945c00ee2836da. This commit caused grievous harm to old notices on identi.ca. Reverting until we figure out how to convert the old notices. --- classes/Memcached_DataObject.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 877bbf2e0f..5f71f716b3 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -227,20 +227,4 @@ class Memcached_DataObject extends DB_DataObject $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry); return new ArrayWrapper($cached); } - - // We overload so that 'SET NAMES "utf8"' is called for - // each connection - - function _connect() - { - global $_DB_DATAOBJECT; - $exists = !empty($this->_database_dsn_md5) && - isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]); - $result = parent::_connect(); - if (!$exists) { - $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]; - $DB->query('SET NAMES "utf8"'); - } - return $result; - } } From ccf45d454c68f7f667d07e0db608569e049ec285 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 15:08:49 -0400 Subject: [PATCH 33/83] Lots of tweaking to make things work Did some tweaking and maneuvering to make things work. This version will now show a "notice received" alert box -- lots of progress! Had to test with Java server, not Python server. --- plugins/Comet/CometPlugin.php | 22 +- plugins/Comet/json2.js | 478 ++++++++++++++++++++++++++++++++ plugins/Comet/updatetimeline.js | 48 ++-- 3 files changed, 516 insertions(+), 32 deletions(-) create mode 100644 plugins/Comet/json2.js diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 10f8c198c3..f60d400751 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -56,6 +56,8 @@ class CometPlugin extends Plugin { $timeline = null; + $this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action')); + switch ($action->trimmed('action')) { case 'public': $timeline = '/timelines/public'; @@ -64,16 +66,18 @@ class CometPlugin extends Plugin return true; } - $action->element('script', array('type' => 'text/javascript', - 'src' => common_path('plugins/Comet/jquery.comet.js')), + $scripts = array('jquery.comet.js', 'json2.js', 'updatetimeline.js'); + + foreach ($scripts as $script) { + $action->element('script', array('type' => 'text/javascript', + 'src' => common_path('plugins/Comet/'.$script)), ' '); + } + $action->elementStart('script', array('type' => 'text/javascript')); - $action->raw("var _timelineServer = \"$this->server\"; ". - "var _timeline = \"$timeline\";"); + $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\");});"); $action->elementEnd('script'); - $action->element('script', array('type' => 'text/javascript', - 'src' => common_path('plugins/Comet/updatetimeline.js')), - ' '); + return true; } @@ -96,21 +100,17 @@ class CometPlugin extends Plugin $json = $this->noticeAsJson($notice); - $this->log(LOG_DEBUG, "JSON = '$json'"); - // Bayeux? Comet? Huh? These terms confuse me $bay = new Bayeux($this->server); foreach ($timelines as $timeline) { $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'."); $bay->publish($timeline, $json); - $this->log(LOG_DEBUG, "Done posting notice $notice->id to '$timeline'."); } $bay = NULL; } - $this->log(LOG_DEBUG, "All done."); return true; } diff --git a/plugins/Comet/json2.js b/plugins/Comet/json2.js new file mode 100644 index 0000000000..7e27df5181 --- /dev/null +++ b/plugins/Comet/json2.js @@ -0,0 +1,478 @@ +/* + http://www.JSON.org/json2.js + 2009-04-16 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the object holding the key. + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. +*/ + +/*jslint evil: true */ + +/*global JSON */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + JSON = {}; +} +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index 6612b51168..7b22445e30 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -3,28 +3,34 @@ var updater = function() { - var _handshook = false; - var _connected = false; - var _cometd; + var _cometd; - return { - init: function() - { - _cometd = $.cometd; // Uses the default Comet object - _cometd.init(_timelineServer); - _cometd.subscribe(_timeline, this, receive); - $(window).unload(leave); - } - } + return { + init: function(server, timeline) + { + _cometd = $.cometd; // Uses the default Comet object + _cometd.setLogLevel('debug'); + _cometd.init(server); + _cometd.subscribe(timeline, receive); + $(window).unload(leave); + } + } - function leave() - { - _cometd.disconnect(); - } + function leave() + { + _cometd.disconnect(); + } - function receive(message) - { - var noticeItem = makeNoticeItem(message.data); - var noticeList = $('ul.notices'); - } + function receive(message) + { + alert("Received notice."); + var noticeItem = makeNoticeItem(message.data); + var noticeList = $('ul.notices'); + } + + function makeNoticeItem(data) + { + return ''; + } }(); + From 7dbb5fb8fdf7c4f82c212863a17793a50f887f58 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 15:37:00 -0400 Subject: [PATCH 34/83] Make notice auto-update Shows notices auto-updating --- plugins/Comet/CometPlugin.php | 5 +++++ plugins/Comet/updatetimeline.js | 37 ++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index f60d400751..a7a4f4b237 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -126,6 +126,11 @@ class CometPlugin extends Plugin $act = new TwitterApiAction('/dev/null'); $arr = $act->twitter_status_array($notice, true); + $arr['url'] = $notice->bestUrl(); + + $profile = $notice->getProfile(); + $arr['user']['profile_url'] = $profile->profileurl; + return $arr; } diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index 7b22445e30..c6eefb4475 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -23,14 +23,45 @@ var updater = function() function receive(message) { - alert("Received notice."); var noticeItem = makeNoticeItem(message.data); - var noticeList = $('ul.notices'); + $("#notices_primary .notices").prepend(noticeItem, true); + $("#notices_primary .notice:first").css({display:"none"}); + $("#notices_primary .notice:first").fadeIn(2500); + NoticeHover(); + NoticeReply(); } function makeNoticeItem(data) { - return ''; + user = data['user']; + ni = "
  • "+ + "
    "+ + ""+ + ""+ + "\""+user['screen_name']+"\"/"+ + ""+user['screen_name']+""+ + ""+ + ""+ + "

    "+data['text']+"

    "+ + "
    "+ + "
    "+ + "
    "+ + "
    Published
    "+ + "
    "+ + ""+ + "a few seconds ago"+ + " "+ + "
    "+ + "
    "+ + "
    "+ + "
    From
    "+ + "
    "+data['source']+"
    "+ + "
    "+ + "
    "+ + "
    "+ + "
    "+ + "
  • "; + return ni; } }(); From 781341d91fd4c7406d8687e7828ab86f9696cf66 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 15:41:55 -0400 Subject: [PATCH 35/83] README for the comet plugin --- plugins/Comet/README | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 plugins/Comet/README diff --git a/plugins/Comet/README b/plugins/Comet/README new file mode 100644 index 0000000000..4abd40af7a --- /dev/null +++ b/plugins/Comet/README @@ -0,0 +1,26 @@ +This is a plugin to automatically load notices in the browser no +matter who creates them -- the kind of thing we see with +search.twitter.com, rejaw.com, or FriendFeed's "real time" news. + +NOTE: this is an insecure version; don't roll it out on a production +server. + +It requires a cometd server. I've only had the cometd-java server work +correctly; something's wiggy with the Twisted-based server. + +After you have a cometd server installed, just add this code to your +config.php: + + require_once(INSTALLDIR.'/plugins/Comet/CometPlugin.php'); + $cp = new CometPlugin('http://example.com:8080/cometd/'); + +Change 'example.com:8080' to the name and port of the server you +installed cometd on. + +TODO: + +* Needs to be tested with Ajax submission. Probably messes everything + up. +* Add more timelines: personal inbox and tags would be great. +* Add security. In particular, only let the PHP code publish notices + to the cometd server. Currently, it doesn't try to authenticate. From e438334c00ebe29c01bfc5b02aa64cffdb43cb46 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 18:00:06 -0400 Subject: [PATCH 36/83] add live updating for tag pages --- plugins/Comet/CometPlugin.php | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index a7a4f4b237..cff0d4c9d6 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -62,6 +62,14 @@ class CometPlugin extends Plugin case 'public': $timeline = '/timelines/public'; break; + case 'tag': + $tag = $action->trimmed('tag'); + if (!empty($tag)) { + $timeline = '/timelines/tag/'.$tag; + } else { + return true; + } + break; default: return true; } @@ -94,6 +102,14 @@ class CometPlugin extends Plugin $timelines[] = '/timelines/public'; } + $tags = $this->getNoticeTags($notice); + + if (!empty($tags)) { + foreach ($tags as $tag) { + $timelines[] = '/timelines/tag/' . $tag; + } + } + if (count($timelines) > 0) { // Require this, since we need it require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php'); @@ -134,6 +150,26 @@ class CometPlugin extends Plugin return $arr; } + function getNoticeTags($notice) + { + $tags = null; + + $nt = new Notice_tag(); + $nt->notice_id = $notice->id; + + if ($nt->find()) { + $tags = array(); + while ($nt->fetch()) { + $tags[] = $nt->tag; + } + } + + $nt->free(); + $nt = null; + + return $tags; + } + // Push this up to Plugin function log($level, $msg) From db3b56a2fdf51e97e9859aa731674947571667aa Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 20:50:39 -0400 Subject: [PATCH 37/83] Display rendered HTML for a notice Display the rendered HTML for a notice --- plugins/Comet/CometPlugin.php | 1 + plugins/Comet/updatetimeline.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index cff0d4c9d6..2e0bb40a46 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -143,6 +143,7 @@ class CometPlugin extends Plugin $arr = $act->twitter_status_array($notice, true); $arr['url'] = $notice->bestUrl(); + $arr['html'] = htmlspecialchars($notice->rendered); $profile = $notice->getProfile(); $arr['user']['profile_url'] = $profile->profileurl; diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index c6eefb4475..55511d35ff 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -34,6 +34,8 @@ var updater = function() function makeNoticeItem(data) { user = data['user']; + html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + ni = "
  • "+ "
    "+ ""+ @@ -42,7 +44,7 @@ var updater = function() ""+user['screen_name']+""+ ""+ ""+ - "

    "+data['text']+"

    "+ + "

    "+html+"

    "+ "
    "+ "
    "+ "
    "+ From e97223b2ba3f9f1818ba12b707c53c0ebdcab144 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 21:15:11 -0400 Subject: [PATCH 38/83] Don't add a notice if it already exists on the page Try not to interfere with Ajax posting; don't show something if it's already on the page. --- plugins/Comet/updatetimeline.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index 55511d35ff..de750baba3 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -23,6 +23,14 @@ var updater = function() function receive(message) { + id = message.data.id; + + // Don't add it if it already exists + + if ($("#notice-"+id).length > 0) { + return; + } + var noticeItem = makeNoticeItem(message.data); $("#notices_primary .notices").prepend(noticeItem, true); $("#notices_primary .notice:first").css({display:"none"}); From 7405d9dfa684975309150537069a1268a67ed6be Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 26 Apr 2009 21:16:09 -0400 Subject: [PATCH 39/83] Don't add a node if it's already there Try not to double-add a node on Ajax submit. Normally not a big deal, but may happen if the CometPlugin (or in the future Strophe or other auto-update plugins) is enabled. --- js/util.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/js/util.js b/js/util.js index 15a14625c7..f15c4f2bbf 100644 --- a/js/util.js +++ b/js/util.js @@ -188,11 +188,15 @@ $(document).ready(function(){ alert(result); } else { - $("#notices_primary .notices").prepend(document._importNode($("li", xml).get(0), true)); - $("#notices_primary .notice:first").css({display:"none"}); - $("#notices_primary .notice:first").fadeIn(2500); - NoticeHover(); - NoticeReply(); + li = $("li", xml).get(0); + id = li.id; + if ($("#"+li.id).length == 0) { + $("#notices_primary .notices").prepend(document._importNode(li, true)); + $("#notices_primary .notice:first").css({display:"none"}); + $("#notices_primary .notice:first").fadeIn(2500); + NoticeHover(); + NoticeReply(); + } } $("#notice_data-text").val(""); counter(); From efe8c47d7d9e91eb8308845b8b0717e84e36e346 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 27 Apr 2009 20:07:22 +0000 Subject: [PATCH 40/83] Minor CSS order/cleanup. --- theme/base/css/display.css | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 0bc2e68b65..dc6b4bc29a 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -842,23 +842,6 @@ text-transform:lowercase; } - -.notice-data { -position:absolute; -top:18px; -right:0; -min-height:50px; -margin-bottom:4px; -} -.notice .entry-content .notice-data dt { -display:none; -} - -.notice-data a { -display:block; -outline:none; -} - .notice-options { padding-left:2%; float:left; @@ -1036,6 +1019,8 @@ padding-right:30px; .hentry .entry-content p { margin-bottom:18px; } +.system_notice ul, +.instructions ul, .hentry entry-content ol, .hentry .entry-content ul { list-style-position:inside; @@ -1160,9 +1145,6 @@ clear:both; margin-bottom:0; } -.instructions ul { -list-style-position:inside; -} .instructions p, .instructions ul { margin-bottom:18px; From 6a20ef71d3b2b325ce24318e2ba4483d6c8732ce Mon Sep 17 00:00:00 2001 From: CiaranG Date: Tue, 28 Apr 2009 13:05:48 +0100 Subject: [PATCH 41/83] Fixed typo in stopdaemons.sh - was not stopping the new memcached queue handler --- scripts/stopdaemons.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index 196991de0f..f6d71eddfb 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ - memcachedhandler inboxhandler; do + memcachehandler inboxhandler; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do From 5b78f95e972f9f19ea46607e8b9544b8f7c4207a Mon Sep 17 00:00:00 2001 From: CiaranG Date: Tue, 28 Apr 2009 13:30:54 +0100 Subject: [PATCH 42/83] Only start daemons that are required, according to the site config. There is the potential to not start some more - see the checks in getvaliddaemons.php --- scripts/getvaliddaemons.php | 52 +++++++++++++++++++++++++++++++++++++ scripts/startdaemons.sh | 6 ++--- 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100755 scripts/getvaliddaemons.php diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php new file mode 100755 index 0000000000..482e63af70 --- /dev/null +++ b/scripts/getvaliddaemons.php @@ -0,0 +1,52 @@ +#!/usr/bin/env php +. + */ + +/** + * Utility script to get a list of daemons that should run, based on the + * current configuration. This is used by startdaemons.sh to determine what + * it should and shouldn't start up. The output is a list of space-separated + * daemon names. + */ + + +# Abort if called from a web server +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); +define('LACONICA', true); + +require_once(INSTALLDIR . '/lib/common.php'); + +if(common_config('xmpp','enabled')) { + echo "xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php "; + echo "xmppconfirmhandler.php "; +} +if(common_config('memcached','enabled')) { + echo "memcachedqueuehandler.php "; +} +echo "ombqueuehandler.php "; +echo "twitterqueuehandler.php "; +echo "facebookqueuehandler.php "; +echo "pingqueuehandler.php "; +echo "inboxqueuehandler.php "; +echo "smsqueuehandler.php "; diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh index 66f9ed4e0c..3869e95c4c 100755 --- a/scripts/startdaemons.sh +++ b/scripts/startdaemons.sh @@ -21,11 +21,9 @@ # Note that the 'maildaemon' needs to run as a mail filter. DIR=`dirname $0` +DAEMONS=`php $DIR/getvaliddaemons.php` -for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \ - xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \ - twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php \ - memcachedqueuehandler.php inboxqueuehandler.php; do +for f in $DAEMONS; do echo -n "Starting $f..."; php $DIR/$f From c7105c2af1ebe3cddd477265c6fea59a61d0c7e5 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 28 Apr 2009 13:07:05 -0400 Subject: [PATCH 43/83] Change to avoid a join in notice inbox The join in notice_inbox is causing temp-table sorting on identi.ca, so I'm trying a finer-tuned approach. --- classes/Notice.php | 31 ++++++++++-- classes/Notice_inbox.php | 101 ++++++++++++++++++++++++++++++++++++++- classes/User.php | 29 +---------- 3 files changed, 127 insertions(+), 34 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 27b98de1cc..b087c94bc4 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -215,6 +215,7 @@ class Notice extends Memcached_DataObject if (common_config('queue', 'enabled')) { $notice->blowAuthorCaches(); } else { + common_debug("Blowing caches for new notice."); $notice->blowCaches(); } } @@ -285,7 +286,7 @@ class Notice extends Memcached_DataObject // Clear the user's cache $cache = common_memcache(); if (!empty($cache)) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $this->profile_id)); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id)); } $this->blowNoticeCache($blowLast); $this->blowPublicCache($blowLast); @@ -307,9 +308,9 @@ class Notice extends Memcached_DataObject $member->group_id = $group_inbox->group_id; if ($member->find()) { while ($member->fetch()) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id)); + $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id . ';last')); + $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last')); } } } @@ -352,9 +353,9 @@ class Notice extends Memcached_DataObject 'WHERE subscription.subscribed = ' . $this->profile_id); while ($user->fetch()) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last')); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user_id.';last')); } } $user->free(); @@ -613,6 +614,26 @@ class Notice extends Memcached_DataObject return $wrapper; } + function getStreamByIds($ids) + { + $cache = common_memcache(); + + if (!empty($cache)) { + $notices = array(); + foreach ($ids as $id) { + $notices[] = Notice::staticGet('id', $id); + } + return new ArrayWrapper($notices); + } else { + $notice = new Notice(); + $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); + $notice->orderBy('id DESC'); + + $notice->find(); + return $notice; + } + } + function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) { diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index 81ddb45385..162da74fee 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -1,7 +1,7 @@ INBOX_CACHE_WINDOW) { + common_debug('Doing direct DB hit for notice_inbox since the params are screwy.'); + return Notice_inbox::_streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since); + } + + $idkey = common_cache_key('notice_inbox:by_user:'.$user_id); + + $idstr = $cache->get($idkey); + + if (!empty($idstr)) { + // Cache hit! Woohoo! + common_debug('Cache hit for notice_inbox.'); + $window = explode(',', $idstr); + $ids = array_slice($window, $offset, $limit); + return $ids; + } + + $laststr = common_cache_key($idkey.';last'); + + if (!empty($laststr)) { + common_debug('Cache hit for notice_inbox on last item.'); + + $window = explode(',', $laststr); + $last_id = $window[0]; + $new_ids = Notice_inbox::_streamDirect($user_id, 0, INBOX_CACHE_WINDOW, + $last_id, null, null); + + $new_window = array_merge($new_ids, $window); + + $new_windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $new_windowstr); + $result = $cache->set($idkey . ';last', $new_windowstr); + + $ids = array_slice($new_window, $offset, $limit); + + return $ids; + } + + $window = Notice_inbox::_streamDirect($user_id, 0, INBOX_CACHE_WINDOW, + null, null, null); + + $windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $windowstr); + $result = $cache->set($idkey . ';last', $windowstr); + + $ids = array_slice($window, $offset, $limit); + + return $ids; + } + + function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since) + { + $inbox = new Notice_inbox(); + + $inbox->user_id = $user_id; + + if ($since_id != 0) { + $inbox->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $inbox->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $inbox->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $inbox->limit($offset, $limit); + } + + $ids = array(); + + if ($inbox->find()) { + while ($inbox->fetch()) { + $ids[] = $inbox->notice_id; + } + } + + return $ids; + } } diff --git a/classes/User.php b/classes/User.php index 098381f738..ce7ea1464f 100644 --- a/classes/User.php +++ b/classes/User.php @@ -451,34 +451,9 @@ class User extends Memcached_DataObject } else if ($enabled === true || ($enabled == 'transitional' && $this->inboxed == 1)) { - $cache = common_memcache(); + $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since); - if (!empty($cache)) { - - # Get the notices out of the cache - - $notices = $cache->get(common_cache_key($cachekey)); - - # On a cache hit, return a DB-object-like wrapper - - if ($notices !== false) { - $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit)); - return $wrapper; - } - } - - $inbox = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since); - - $ids = array(); - - while ($inbox->fetch()) { - $ids[] = $inbox->notice_id; - } - - $inbox->free(); - unset($inbox); - - return Notice::getStreamByIds($ids, 'user:notices_with_friends:' . $this->id); + return Notice::getStreamByIds($ids); } } From fe53e780be5db4ceb2831a1d69faec6130a10deb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 28 Apr 2009 13:31:56 -0400 Subject: [PATCH 44/83] Remove some debug comments in query-by-id --- classes/Notice.php | 5 ++--- classes/Notice_inbox.php | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index b087c94bc4..49d0939c1a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -215,7 +215,6 @@ class Notice extends Memcached_DataObject if (common_config('queue', 'enabled')) { $notice->blowAuthorCaches(); } else { - common_debug("Blowing caches for new notice."); $notice->blowCaches(); } } @@ -353,9 +352,9 @@ class Notice extends Memcached_DataObject 'WHERE subscription.subscribed = ' . $this->profile_id); while ($user->fetch()) { - $cache->delete(common_cache_key('notice_inbox:by_user:'.$user_id)); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id)); if ($blowLast) { - $cache->delete(common_cache_key('notice_inbox:by_user:'.$user_id.';last')); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last')); } } $user->free(); diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index 162da74fee..f321370380 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -50,7 +50,6 @@ class Notice_inbox extends Memcached_DataObject if (empty($cache) || $since_id != 0 || $before_id != 0 || !is_null($since) || ($offset + $limit) > INBOX_CACHE_WINDOW) { - common_debug('Doing direct DB hit for notice_inbox since the params are screwy.'); return Notice_inbox::_streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since); } @@ -60,7 +59,6 @@ class Notice_inbox extends Memcached_DataObject if (!empty($idstr)) { // Cache hit! Woohoo! - common_debug('Cache hit for notice_inbox.'); $window = explode(',', $idstr); $ids = array_slice($window, $offset, $limit); return $ids; @@ -69,8 +67,6 @@ class Notice_inbox extends Memcached_DataObject $laststr = common_cache_key($idkey.';last'); if (!empty($laststr)) { - common_debug('Cache hit for notice_inbox on last item.'); - $window = explode(',', $laststr); $last_id = $window[0]; $new_ids = Notice_inbox::_streamDirect($user_id, 0, INBOX_CACHE_WINDOW, From f798d1ea430d25c2f4a60179c65b39b1257a5340 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 28 Apr 2009 17:08:20 -0700 Subject: [PATCH 45/83] Added dirty dates to Foreign_link --- classes/Foreign_link.php | 2 ++ classes/laconica.ini | 2 ++ db/laconica.sql | 2 ++ 3 files changed, 6 insertions(+) diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php index 5d9c82a85a..6065609512 100644 --- a/classes/Foreign_link.php +++ b/classes/Foreign_link.php @@ -17,6 +17,8 @@ class Foreign_link extends Memcached_DataObject public $noticesync; // tinyint(1) not_null default_1 public $friendsync; // tinyint(1) not_null default_2 public $profilesync; // tinyint(1) not_null default_1 + public $last_noticesync; // datetime() + public $last_friendsync; // datetime() public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP diff --git a/classes/laconica.ini b/classes/laconica.ini index dd424bbdd3..5a905a4bbe 100755 --- a/classes/laconica.ini +++ b/classes/laconica.ini @@ -55,6 +55,8 @@ credentials = 2 noticesync = 145 friendsync = 145 profilesync = 145 +last_noticesync = 14 +last_friendsync = 14 created = 142 modified = 384 diff --git a/db/laconica.sql b/db/laconica.sql index 83d610f0d3..d9e21a7b51 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -291,6 +291,8 @@ create table foreign_link ( noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies', friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', + last_noticesync datetime default null comment 'last time notices were imported', + last_friendsync datetime default null comment 'last time friends were imported', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', From e85cddba45c2ce02d135f00acdcfa37cb9168130 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 28 Apr 2009 23:31:00 -0700 Subject: [PATCH 46/83] Ticket #1428 - Changed replies API method to "mentions". --- actions/twitapistatuses.php | 38 ++++++++++++++++++++----------------- lib/router.php | 4 ++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/actions/twitapistatuses.php b/actions/twitapistatuses.php index 323c4f1f88..3abeba3672 100644 --- a/actions/twitapistatuses.php +++ b/actions/twitapistatuses.php @@ -144,10 +144,10 @@ class TwitapistatusesAction extends TwitterapiAction break; case 'atom': if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom'; } else { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/friends_timeline.atom'; } $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri); @@ -231,10 +231,10 @@ class TwitapistatusesAction extends TwitterapiAction break; case 'atom': if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom'; } else { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/user_timeline.atom'; } $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri); @@ -344,7 +344,7 @@ class TwitapistatusesAction extends TwitterapiAction $this->show($args, $apidata); } - function replies($args, $apidata) + function mentions($args, $apidata) { parent::handle($args); @@ -360,11 +360,13 @@ class TwitapistatusesAction extends TwitterapiAction $profile = $user->getProfile(); $sitename = common_config('site', 'name'); - $title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname); + $title = sprintf(_('%1$s / Updates mentioning %2$s'), + $sitename, $user->nickname); $taguribase = common_config('integration', 'taguri'); - $id = "tag:$taguribase:Replies:".$user->id; + $id = "tag:$taguribase:Mentions:".$user->id; $link = common_local_url('replies', array('nickname' => $user->nickname)); - $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName()); + $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), + $sitename, $user->nickname, $profile->getBestName()); if (!$page) { $page = 1; @@ -385,7 +387,8 @@ class TwitapistatusesAction extends TwitterapiAction $since = strtotime($this->arg('since')); - $notice = $user->getReplies((($page-1)*20), $count, $since_id, $before_id, $since); + $notice = $user->getReplies((($page-1)*20), + $count, $since_id, $before_id, $since); $notices = array(); while ($notice->fetch()) { @@ -400,14 +403,10 @@ class TwitapistatusesAction extends TwitterapiAction $this->show_rss_timeline($notices, $title, $link, $subtitle); break; case 'atom': - if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . - 'api/statuses/replies/' . $apidata['api_arg'] . '.atom'; - } else { - $selfuri = $selfuri = common_root_url() . - 'api/statuses/replies.atom'; - } - $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, null, $selfuri); + $selfuri = common_root_url() . + ltrim($_SERVER['QUERY_STRING'], 'p='); + $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, + null, $selfuri); break; case 'json': $this->show_json_timeline($notices); @@ -418,6 +417,11 @@ class TwitapistatusesAction extends TwitterapiAction } + function replies($args, $apidata) + { + call_user_func(array($this, 'mentions'), $args, $apidata); + } + function show($args, $apidata) { parent::handle($args); diff --git a/lib/router.php b/lib/router.php index 6fb2f94872..12590b790d 100644 --- a/lib/router.php +++ b/lib/router.php @@ -231,12 +231,12 @@ class Router $m->connect('api/statuses/:method', array('action' => 'api', 'apiaction' => 'statuses'), - array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?')); + array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?')); $m->connect('api/statuses/:method/:argument', array('action' => 'api', 'apiaction' => 'statuses'), - array('method' => '(user_timeline|friends_timeline|replies|show|destroy|friends|followers)')); + array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); // users From 10ef8a2f7112a83188e9702d480abd3c6062c26c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 29 Apr 2009 11:27:45 -0400 Subject: [PATCH 47/83] Move algorithm for caching to Notice class Moved the algorithm for notice stream caching to the Notice class. --- classes/Notice.php | 55 +++++++++++++++++++++++++++++++++++++++ classes/Notice_inbox.php | 56 ++++------------------------------------ 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 49d0939c1a..faabb1e140 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -967,4 +967,59 @@ class Notice extends Memcached_DataObject array('notice' => $this->id)); } } + + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) + { + $cache = common_memcache(); + + if (empty($cache) || + $since_id != 0 || $before_id != 0 || !is_null($since) || + ($offset + $limit) > NOTICE_CACHE_WINDOW) { + return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, + $before_id, $since))); + } + + $idkey = common_cache_key($cachekey); + + $idstr = $cache->get($idkey); + + if (!empty($idstr)) { + // Cache hit! Woohoo! + $window = explode(',', $idstr); + $ids = array_slice($window, $offset, $limit); + return $ids; + } + + $laststr = common_cache_key($idkey.';last'); + + if (!empty($laststr)) { + $window = explode(',', $laststr); + $last_id = $window[0]; + $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, + $last_id, 0, null))); + + $new_window = array_merge($new_ids, $window); + + $new_windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $new_windowstr); + $result = $cache->set($idkey . ';last', $new_windowstr); + + $ids = array_slice($new_window, $offset, $limit); + + return $ids; + } + + $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, + 0, 0, null))); + + $windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $windowstr); + $result = $cache->set($idkey . ';last', $windowstr); + + $ids = array_slice($window, $offset, $limit); + + return $ids; + } } diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index f321370380..dec14b0d18 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -43,58 +43,12 @@ class Notice_inbox extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - function stream($user_id, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) + function stream($user_id, $offset, $limit, $since_id, $before_id, $since) { - $cache = common_memcache(); - - if (empty($cache) || - $since_id != 0 || $before_id != 0 || !is_null($since) || - ($offset + $limit) > INBOX_CACHE_WINDOW) { - return Notice_inbox::_streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since); - } - - $idkey = common_cache_key('notice_inbox:by_user:'.$user_id); - - $idstr = $cache->get($idkey); - - if (!empty($idstr)) { - // Cache hit! Woohoo! - $window = explode(',', $idstr); - $ids = array_slice($window, $offset, $limit); - return $ids; - } - - $laststr = common_cache_key($idkey.';last'); - - if (!empty($laststr)) { - $window = explode(',', $laststr); - $last_id = $window[0]; - $new_ids = Notice_inbox::_streamDirect($user_id, 0, INBOX_CACHE_WINDOW, - $last_id, null, null); - - $new_window = array_merge($new_ids, $window); - - $new_windowstr = implode(',', $new_window); - - $result = $cache->set($idkey, $new_windowstr); - $result = $cache->set($idkey . ';last', $new_windowstr); - - $ids = array_slice($new_window, $offset, $limit); - - return $ids; - } - - $window = Notice_inbox::_streamDirect($user_id, 0, INBOX_CACHE_WINDOW, - null, null, null); - - $windowstr = implode(',', $new_window); - - $result = $cache->set($idkey, $windowstr); - $result = $cache->set($idkey . ';last', $windowstr); - - $ids = array_slice($window, $offset, $limit); - - return $ids; + return Notice::stream(array('Notice_inbox', '_streamDirect'), + array($user_id), + 'notice_inbox:by_user:'.$user_id, + $offset, $limit, $since_id, $before_id, $since); } function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since) From a4d959b8a2254d173bdf45f418f6add9f6f62cda Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 29 Apr 2009 12:05:31 -0400 Subject: [PATCH 48/83] Public stream uses IDs method Public stream now uses IDs method --- classes/Notice.php | 55 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index faabb1e140..6a7522bd32 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -635,25 +635,58 @@ class Notice extends Memcached_DataObject function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) { + $ids = Notice::stream(array('Notice', '_publicStreamDirect'), + array(), + 'public', + $offset, $limit, $since_id, $before_id, $since); - $parts = array(); + return Notice::getStreamByIds($ids); + } - $qry = 'SELECT * FROM notice '; + function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->orderBy('id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } if (common_config('public', 'localonly')) { - $parts[] = 'is_local = 1'; + $notice->whereAdd('is_local = 1'); } else { # -1 == blacklisted - $parts[] = 'is_local != -1'; + $notice->whereAdd('is_local != -1'); } - if ($parts) { - $qry .= ' WHERE ' . implode(' AND ', $parts); + if ($since_id != 0) { + $notice->whereAdd('id > ' . $since_id); } - return Notice::getStream($qry, - 'public', - $offset, $limit, $since_id, $before_id, null, $since); + if ($before_id != 0) { + $notice->whereAdd('id < ' . $before_id); + } + + if (!is_null($since)) { + $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; } function addToInboxes() @@ -990,7 +1023,7 @@ class Notice extends Memcached_DataObject return $ids; } - $laststr = common_cache_key($idkey.';last'); + $laststr = $cache->get($idkey.';last'); if (!empty($laststr)) { $window = explode(',', $laststr); @@ -1013,7 +1046,7 @@ class Notice extends Memcached_DataObject $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, 0, 0, null))); - $windowstr = implode(',', $new_window); + $windowstr = implode(',', $window); $result = $cache->set($idkey, $windowstr); $result = $cache->set($idkey . ';last', $windowstr); From 1e8ea1eb460b163176c4d7d1e7dffa500024ef91 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 29 Apr 2009 16:09:03 -0400 Subject: [PATCH 49/83] Make the tag stream use ID mechanism --- classes/Notice.php | 5 +--- classes/Notice_tag.php | 59 +++++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 6a7522bd32..2bb466155a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -328,10 +328,7 @@ class Notice extends Memcached_DataObject $tag->notice_id = $this->id; if ($tag->find()) { while ($tag->fetch()) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag)); - if ($blowLast) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last')); - } + $tag->blowCache($blowLast); } } $tag->free(); diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index f2247299a4..e5b7722430 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -37,21 +37,62 @@ class Notice_tag extends Memcached_DataObject ###END_AUTOCODE static function getStream($tag, $offset=0, $limit=20) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' . - "WHERE notice_tag.tag = '%s' "; - return Notice::getStream(sprintf($qry, $tag), - 'notice_tag:notice_stream:' . common_keyize($tag), - $offset, $limit); + $ids = Notice::stream(array('Notice_tag', '_streamDirect'), + array($tag), + 'notice_tag:notice_ids:' . common_keyize($tag), + $offset, $limit); + + return Notice::getStreamByIds($ids); } - function blowCache() + function _streamDirect($tag, $offset, $limit, $since_id, $before_id, $since) + { + $nt = new Notice_tag(); + + $nt->tag = $tag; + + $nt->selectAdd(); + $nt->selectAdd('notice_id'); + + if ($since_id != 0) { + $nt->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $nt->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $nt->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $nt->limit($offset, $limit); + } + + $ids = array(); + + if ($nt->find()) { + while ($nt->fetch()) { + $ids[] = $nt->notice_id; + } + } + + return $ids; + } + + function blowCache($blowLast=false) { $cache = common_memcache(); if ($cache) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag)); + $idkey = common_cache_key('notice_tag:notice_ids:' . common_keyize($this->tag)); + $cache->delete($idkey); + if ($blowLast) { + $cache->delete($idkey.';last'); + } } } From 8295402fb30a3854bae3b9d6c457c7c0e432c51a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 29 Apr 2009 13:16:52 -0700 Subject: [PATCH 50/83] Added 'mentions' the the list of API methods requiring bare auth --- actions/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/api.php b/actions/api.php index d2f0a2eff0..8762b4bcd3 100644 --- a/actions/api.php +++ b/actions/api.php @@ -130,6 +130,7 @@ class ApiAction extends Action 'statuses/friends_timeline', 'statuses/friends', 'statuses/replies', + 'statuses/mentions', 'statuses/followers', 'favorites/favorites'); From b79fef307481b36b4d04dbabb54e3f6d9edf6896 Mon Sep 17 00:00:00 2001 From: CiaranG Date: Wed, 29 Apr 2009 23:43:42 +0100 Subject: [PATCH 51/83] Fixed remote subscription, broken in fc6cedd2227d9d560736e494f431e2b40b26b45c --- actions/accesstoken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/accesstoken.php b/actions/accesstoken.php index bb68d3314d..46b43c7021 100644 --- a/actions/accesstoken.php +++ b/actions/accesstoken.php @@ -59,7 +59,7 @@ class AccesstokenAction extends Action try { common_debug('getting request from env variables', __FILE__); common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_locale_url('accesstoken')); + $req = OAuthRequest::from_request('POST', common_local_url('accesstoken')); common_debug('getting a server', __FILE__); $server = omb_oauth_server(); common_debug('fetching the access token', __FILE__); From aee641ee1e311fb0af0f9f6d75ca7fae2c7d8477 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 29 Apr 2009 20:45:33 -0400 Subject: [PATCH 52/83] make replies use new query format --- classes/Notice.php | 4 ++-- classes/Reply.php | 47 ++++++++++++++++++++++++++++++++++++++++++++-- classes/User.php | 10 +++------- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 2bb466155a..808631f4dc 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -380,9 +380,9 @@ class Notice extends Memcached_DataObject $reply->notice_id = $this->id; if ($reply->find()) { while ($reply->fetch()) { - $cache->delete(common_cache_key('user:replies:'.$reply->profile_id)); + $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last')); + $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last')); } } } diff --git a/classes/Reply.php b/classes/Reply.php index af86aaf878..4439053b44 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -4,7 +4,7 @@ */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Reply extends Memcached_DataObject +class Reply extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -13,7 +13,7 @@ class Reply extends Memcached_DataObject public $notice_id; // int(4) primary_key not_null public $profile_id; // int(4) primary_key not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP - public $replied_id; // int(4) + public $replied_id; // int(4) /* Static get */ function staticGet($k,$v=null) @@ -21,4 +21,47 @@ class Reply extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + { + $ids = Notice::stream(array('Reply', '_streamDirect'), + array($user_id), + 'reply:stream:' . $user_id, + $offset, $limit, $since_id, $before_id, $since); + return $ids; + } + + function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + { + $reply = new Reply(); + $reply->profile_id = $user_id; + + if ($since_id != 0) { + $reply->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $reply->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $reply->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $reply->limit($offset, $limit); + } + + $ids = array(); + + if ($reply->find()) { + while ($reply->fetch()) { + $ids[] = $reply->notice_id; + } + } + + return $ids; + } } diff --git a/classes/User.php b/classes/User.php index ce7ea1464f..b76e45c330 100644 --- a/classes/User.php +++ b/classes/User.php @@ -401,13 +401,9 @@ class User extends Memcached_DataObject function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN reply ON notice.id = reply.notice_id ' . - 'WHERE reply.profile_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'user:replies:'.$this->id, - $offset, $limit, $since_id, $before_id, null, $since); + $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since); + common_debug("Ids = " . implode(',', $ids)); + return Notice::getStreamByIds($ids); } function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) From fb8340fb5429e632841b0be66d63dbdc512382ca Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 1 May 2009 03:50:24 +0000 Subject: [PATCH 53/83] Added laconica logo Updated installation page markup --- install.php | 161 ++++++++++++++++++++++++----------------- theme/default/logo.png | Bin 0 -> 2228 bytes 2 files changed, 93 insertions(+), 68 deletions(-) create mode 100644 theme/default/logo.png diff --git a/install.php b/install.php index 87a99a6508..66e8e87124 100644 --- a/install.php +++ b/install.php @@ -52,23 +52,21 @@ function checkPrereqs() foreach ($reqs as $req) { if (!checkExtension($req)) { - ?>

    Cannot load required extension "".

    Cannot load required extension:

    Cannot write config file to "".

    -

    On your server, try this command:

    -
    chmod a+w
    + ?>

    Cannot write config file to:

    +

    On your server, try this command: chmod a+w

    Cannot write avatar directory "/avatar/".

    -

    On your server, try this command:

    -
    chmod a+w /avatar/
    + ?>

    Cannot write avatar directory: /avatar/

    +

    On your server, try this command: chmod a+w /avatar/

    -

    Enter your database connection information below to initialize the database.

    -
    -
    -
      -
    • - - -

      The name of your site

      -
    • -
    • -
    • - - -

      Database hostname

      -
    • -
    • - - -

      Database name

      -
    • -
    • - - -

      Database username

      -
    • -
    • - - -

      Database password

      -
    • -
    - -
    + + +
    +
    +
    Page notice
    +
    +
    +

    Enter your database connection information below to initialize the database.

    +
    +
    +
    + +
    + Connection settings +
      +
    • + + +

      The name of your site

      +
    • +
    • +
    • + + +

      Database hostname

      +
    • +
    • + + +

      Database name

      +
    • +
    • + + +

      Database username

      +
    • +
    • + + +

      Database password

      +
    • +
    + +
    - -
  • - -
  • -> + + -
      - +
      +
      Page notice
      +
      +
        + -
      - - - - Install Laconica - - - - - -
      -
      -
      -

      Install Laconica

      + xml version="1.0" encoding="UTF-8" "; ?> + + + + Install Laconica + + + + + + + +
      + +
      +
      +

      Install Laconica

      -
      -
      -
      - +
      +
      +
      + diff --git a/theme/default/logo.png b/theme/default/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fdead6c4a0a23614552329ee68bb0860b0200a0c GIT binary patch literal 2228 zcmV;l2ut^gP)Px-Zb?KzRCwC$ojq^VNEpZe$?*ZqCphy7Zs}5i(^0Inn@_-|NO4u-G8MZ8O6kx- zv73fA8)=)94#lL}E=EX*D+s2b$Vn(ZiAVwRa)rmo#OIgs%h+S@^OvlMyR|(pzj@}F zXI{L7mg~jrfewJbc-ic)v(Y{BK*CZ>Agq`fYCJt#f)oT$DdJ`wVR0G05yJQd$`Td8s!2tM+d0!>>du}p1(x(x!NT6qM-0u-F6nzq(bB_Q6AF(Dw@E|-AFFPnfMAb2?~ z1mbnm60FolofoqQ$7o=5@8R{j0b#*E>!BGS z{C$D%FF)bOpQm}dlaUEb2#9zfIslHFU=hB*{DdC|pUd(y)=foxDCPEczL-7u2!+Gg z*C$_b@$?<8PcO=T^sWh*Wj$m8 zFlDINO?-R)p{mz)tPB=%XEyHu_=xN~T zWga5`9#|c~ArFO$Kq$3*{l~#)84jd~EGwa+SvOGxhCmvCefB)%HtHw^Ll9@ENr7Jw z3k=HJ9UnNO%2^1dx{L4S#2E@%%P1?sMiYaQ-?g0;m3L8#unXE&!uHGuMfXj&K^QF8?B_NdYAFfZ%l}U@mL9sKN z8#3Re1Q2Ygx^g27g;d#6epM`l0mW2P>>*BkG|^iGAlh^cfK+XFS7a`%@tSTS900rU zCxceu&*UwFTuINh?%GVG7^?fSvY58}<#03A+M^wG;C$H@L#jwfss@#kb-8hQ|`$F;xMGUQFU_2Enw* zhf3JJHfI5{v`Qs_2$t7L3RwplZFg6^T7lp`r}!VC01h^~M=JONuc5|*Umjvv=U>{} z-IZMcsM|v5%qiScZ4aagOf4VMy8Va)$=1OEuonEIRv;JzDsRx4qW*7nOe9)|DS&l; z8QmR_68qJfftZ+p#}=>jxfboQE-l#n;UuKBLP0dTP0U5WM42#9nbH2AB)^@?9@n!bNksR#%$D5~BXFfoDk zI`T^ALx_zy98_094ocj?l~un`b&Kf_z)2KV*M30)o_PD$BXxRhYj=>C5TD=i*6;+U*Gp-vNLu?-iZ?VpDlx1NHu@&)q{@PC|&<>aJ zsOvc4ek}w-)HP1Hpa0#EL7g=XQNJ&uaY}v$1#55CLKjSY64_RP`;Cn6Hv}s)sUT$np=C%*R0BE8aQ}Xm zgo*kPaebwY=tUsC7^=exb}pv{fui2;!QcDnWk_#S`X@0JCR3f5{MdrOH8PZCUj`6S zW@iNd*J?G=n+;rtKVg)2KZDDqUxn%D^@#FxA$AN>P?q;maMxFn z^C4`G>^$P{yI1`i2s(B zKS#EO2&j%D)4!&{hGnuqSC7k= z6O*4+);&HB+mlT1n~2B8iM`+0000 Date: Fri, 1 May 2009 04:39:49 +0000 Subject: [PATCH 54/83] Giving more contrast between the background colour and the laconica logo. --- theme/default/css/display.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 0c8fae166f..1fc99eff7d 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -12,7 +12,7 @@ html, body, a:active { -background-color:#97BFD1; +background-color:#C3D6DF; } body { font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; @@ -30,7 +30,7 @@ input, textarea, select, border-color:#aaa; } #filter_tags ul li { -border-color:#97BFD1; +border-color:#C3D6DF; } .form_settings input.form_action-secondary { @@ -69,7 +69,7 @@ color:#002E6E; border-top-color:#D1D9E4; } .section .profile { -border-top-color:#97BFD1; +border-top-color:#C3D6DF; } #content .notice p.entry-content a:visited { @@ -120,7 +120,7 @@ background-color:#EFF3DC; } #anon_notice { -background-color:#97BFD1; +background-color:#C3D6DF; color:#fff; border-color:#fff; } @@ -163,7 +163,7 @@ color:#fff; .form_user_unsubscribe input.submit, .form_group_leave input.submit, .form_user_authorization input.reject { -background-color:#97BFD1; +background-color:#C3D6DF; } .entity_edit a { From 8a142b272c87678268106df48497b90664be323a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 07:08:01 -0400 Subject: [PATCH 55/83] ignore Eclipse project files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 83a53dfa3f..da6947bfdc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ dataobject.ini *.rej .#* *.swp +.buildpath +.project +.settings From 9cac6413a342445a998773458b215d129c2be2d1 Mon Sep 17 00:00:00 2001 From: Ori Avtalion Date: Fri, 1 May 2009 07:11:28 -0400 Subject: [PATCH 56/83] Add s to user favorite notices --- actions/favoritesrss.php | 2 +- actions/showfavorites.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/favoritesrss.php b/actions/favoritesrss.php index f85bf1b190..6b46b8dec7 100644 --- a/actions/favoritesrss.php +++ b/actions/favoritesrss.php @@ -107,7 +107,7 @@ class FavoritesrssAction extends Rss10Action $c = array('url' => common_local_url('favoritesrss', array('nickname' => $user->nickname)), - 'title' => sprintf(_("%s favorite notices"), $user->nickname), + 'title' => sprintf(_("%s's favorite notices"), $user->nickname), 'link' => common_local_url('showfavorites', array('nickname' => $user->nickname)), diff --git a/actions/showfavorites.php b/actions/showfavorites.php index 6e011d5ca5..eed62a2ab3 100644 --- a/actions/showfavorites.php +++ b/actions/showfavorites.php @@ -74,9 +74,9 @@ class ShowfavoritesAction extends Action function title() { if ($this->page == 1) { - return sprintf(_("%s favorite notices"), $this->user->nickname); + return sprintf(_("%s's favorite notices"), $this->user->nickname); } else { - return sprintf(_("%s favorite notices, page %d"), + return sprintf(_("%s's favorite notices, page %d"), $this->user->nickname, $this->page); } From c5e72e248fa70b5e038c74b73b581884112706d5 Mon Sep 17 00:00:00 2001 From: Ori Avtalion Date: Fri, 1 May 2009 07:12:13 -0400 Subject: [PATCH 57/83] Several whitespace fixes --- actions/openidsettings.php | 4 ++-- actions/recoverpassword.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/openidsettings.php b/actions/openidsettings.php index 92469d20f8..5f59ebc014 100644 --- a/actions/openidsettings.php +++ b/actions/openidsettings.php @@ -67,8 +67,8 @@ class OpenidsettingsAction extends AccountSettingsAction function getInstructions() { - return _('[OpenID](%%doc.openid%%) lets you log into many sites ' . - ' with the same user account. '. + return _('[OpenID](%%doc.openid%%) lets you log into many sites' . + ' with the same user account.'. ' Manage your associated OpenIDs from here.'); } diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index 620fe7eb8e..82263fcd59 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -151,11 +151,11 @@ class RecoverpasswordAction extends Action $this->element('p', null, _('If you\'ve forgotten or lost your' . ' password, you can get a new one sent to' . - ' the email address you have stored ' . + ' the email address you have stored' . ' in your account.')); } else if ($this->mode == 'reset') { $this->element('p', null, - _('You\'ve been identified. Enter a ' . + _('You\'ve been identified. Enter a' . ' new password below. ')); } $this->elementEnd('div'); From 609ac4c22463af88c206968134ab16e75e500edd Mon Sep 17 00:00:00 2001 From: Ori Avtalion Date: Fri, 1 May 2009 07:12:37 -0400 Subject: [PATCH 58/83] Fix link to identi.ca in JavaScript badge --- js/identica-badge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/identica-badge.js b/js/identica-badge.js index 869230b7a4..ffa55ae93e 100644 --- a/js/identica-badge.js +++ b/js/identica-badge.js @@ -128,7 +128,7 @@ var a = document.createElement('A'); a.innerHTML = 'get this'; a.target = '_blank'; - a.href = 'http://identica/doc/badge'; + a.href = 'http://identi.ca/doc/badge'; $.s.f.appendChild(a); $.s.appendChild($.s.f); $.f.getUser(); From a86a0e91a5acb5ea894a3d066f9adf3b1ef305ae Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 08:00:37 -0700 Subject: [PATCH 59/83] add favor, reply, delete buttons for cometed notices --- plugins/Comet/CometPlugin.php | 16 ++++++- plugins/Comet/updatetimeline.js | 74 +++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 2e0bb40a46..48ac9dcad6 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -82,8 +82,22 @@ class CometPlugin extends Plugin ' '); } + $user = common_current_user(); + + if (!empty($user->id)) { + $user_id = $user->id; + } else { + $user_id = 0; + } + + $replyurl = common_local_url('newnotice'); + $favorurl = common_local_url('favor'); + // FIXME: need to find a better way to pass this pattern in + $deleteurl = common_local_url('deletenotice', + array('notice' => '0000000000')); + $action->elementStart('script', array('type' => 'text/javascript')); - $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\");});"); + $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); $action->elementEnd('script'); return true; diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index de750baba3..e89b3bddf7 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -3,14 +3,26 @@ var updater = function() { + var _server; + var _timeline; + var _userid; + var _replyurl; + var _favorurl; + var _deleteurl; var _cometd; return { - init: function(server, timeline) + init: function(server, timeline, userid, replyurl, favorurl, deleteurl) { _cometd = $.cometd; // Uses the default Comet object _cometd.setLogLevel('debug'); _cometd.init(server); + _server = server; + _timeline = timeline; + _userid = userid; + _favorurl = favorurl; + _replyurl = replyurl; + _deleteurl = deleteurl; _cometd.subscribe(timeline, receive); $(window).unload(leave); } @@ -34,7 +46,7 @@ var updater = function() var noticeItem = makeNoticeItem(message.data); $("#notices_primary .notices").prepend(noticeItem, true); $("#notices_primary .notice:first").css({display:"none"}); - $("#notices_primary .notice:first").fadeIn(2500); + $("#notices_primary .notice:first").fadeIn(1000); NoticeHover(); NoticeReply(); } @@ -68,10 +80,64 @@ var updater = function() "
      "+data['source']+"
      "+ "
      "+ ""+ - "
      "+ - "
      "+ + "
      "; + + if (_userid != 0) { + var input = $("form#form_notice fieldset input#token"); + var session_key = input.val(); + ni = ni+makeFavoriteForm(data['id'], session_key); + ni = ni+makeReplyLink(data['id'], data['user']['screen_name']); + if (_userid == data['user']['id']) { + ni = ni+makeDeleteLink(data['id']); + } + } + + ni = ni+"
      "+ ""; return ni; } + + function makeFavoriteForm(id, session_key) + { + var ff; + + ff = "
      "+ + "
      "+ + "Favor this notice"+ // XXX: i18n + ""+ + ""+ + ""+ + "
      "+ + "
      "; + return ff; + } + + function makeReplyLink(id, nickname) + { + var rl; + rl = "
      "+ + "
      Reply to this notice
      "+ + "
      "+ + "Reply "+id+""+ + ""+ + "
      "+ + "
      "; + return rl; + } + + function makeDeleteLink(id) + { + var dl, delurl; + delurl = _deleteurl.replace("0000000000", id); + + dl = "
      "+ + "
      Delete this notice
      "+ + "
      "+ + "Delete"+ + "
      "+ + "
      "; + + return dl; + } }(); From 5affe093aba97a0e4ac559b685a240d568929ffb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 08:39:47 -0700 Subject: [PATCH 60/83] add in_reply_to link and make HTML in source work correctly --- plugins/Comet/CometPlugin.php | 9 +++++++++ plugins/Comet/updatetimeline.js | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 48ac9dcad6..0f2fcd701d 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -158,6 +158,15 @@ class CometPlugin extends Plugin $arr = $act->twitter_status_array($notice, true); $arr['url'] = $notice->bestUrl(); $arr['html'] = htmlspecialchars($notice->rendered); + $arr['source'] = htmlspecialchars($arr['source']); + + if (!empty($notice->reply_to)) { + $reply_to = Notice::staticGet('id', $notice->reply_to); + if (!empty($reply_to)) { + $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); + } + $reply_to = null; + } $profile = $notice->getProfile(); $arr['user']['profile_url'] = $profile->profileurl; diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js index e89b3bddf7..170949e9ba 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Comet/updatetimeline.js @@ -54,7 +54,8 @@ var updater = function() function makeNoticeItem(data) { user = data['user']; - html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); + source = data['source'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); ni = "
    • "+ "
      "+ @@ -77,9 +78,19 @@ var updater = function() ""+ "
      "+ "
      From
      "+ - "
      "+data['source']+"
      "+ - "
      "+ - "
      "+ + "
      "+source+"
      "+ // may have a link, I think + ""; + + if (data['in_reply_to_status_id']) { + ni = ni+"
      "+ + "
      To
      "+ + "
      "+ + "in reply to"+ + "
      "+ + "
      "; + } + + ni = ni+""+ "
      "; if (_userid != 0) { From b12e72ae312488caf7cb1e1a396eb05dd38326a9 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 09:42:38 -0700 Subject: [PATCH 61/83] optionally add a username/password on server side for Comet --- plugins/Comet/CometPlugin.php | 8 +++++--- plugins/Comet/bayeux.class.inc.php | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 0f2fcd701d..45251c66f0 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -45,9 +45,11 @@ class CometPlugin extends Plugin { var $server = null; - function __construct($server=null) + function __construct($server=null, $username=null, $password=null) { - $this->server = $server; + $this->server = $server; + $this->username = $username; + $this->password = $password; parent::__construct(); } @@ -131,7 +133,7 @@ class CometPlugin extends Plugin $json = $this->noticeAsJson($notice); // Bayeux? Comet? Huh? These terms confuse me - $bay = new Bayeux($this->server); + $bay = new Bayeux($this->server, $this->user, $this->password); foreach ($timelines as $timeline) { $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'."); diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php index 785d3e3935..39ad8a8fc6 100644 --- a/plugins/Comet/bayeux.class.inc.php +++ b/plugins/Comet/bayeux.class.inc.php @@ -26,9 +26,12 @@ class Bayeux private $oCurl = ''; private $nNextId = 0; + private $sUser = ''; + private $sPassword = ''; + public $sUrl = ''; - function __construct($sUrl) + function __construct($sUrl, $sUser='', $sPassword='') { $this->sUrl = $sUrl; @@ -43,6 +46,10 @@ class Bayeux curl_setopt($this->oCurl, CURLOPT_POST, 1); curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); + if (!is_null($sUser) && mb_strlen($sUser) > 0) { + curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword"); + } + $this->handShake(); } From deb07487bd0802fa6a89cfc8ddd56af93945eb4c Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 1 May 2009 17:00:36 +0000 Subject: [PATCH 62/83] 60 seconds hard timeout for XHR notice posting. JavaScript throws an alert message to the client if the server doesn't respond back in any way. --- js/util.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/js/util.js b/js/util.js index f15c4f2bbf..3f14bc61c6 100644 --- a/js/util.js +++ b/js/util.js @@ -166,14 +166,20 @@ $(document).ready(function(){ $("#notice_action-submit").addClass("disabled"); return true; }, + timeout: '60000', error: function (xhr, textStatus, errorThrown) { $("#form_notice").removeClass("processing"); $("#notice_action-submit").removeAttr("disabled"); $("#notice_action-submit").removeClass("disabled"); - if ($(".error", xhr.responseXML).length > 0) { - $('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true)); + if (textStatus == "timeout") { + alert ("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists"); } else { - alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again."); + if ($(".error", xhr.responseXML).length > 0) { + $('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true)); + } + else { + alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again."); + } } }, success: function(xml) { if ($("#error", xml).length > 0) { @@ -189,7 +195,6 @@ $(document).ready(function(){ } else { li = $("li", xml).get(0); - id = li.id; if ($("#"+li.id).length == 0) { $("#notices_primary .notices").prepend(document._importNode(li, true)); $("#notices_primary .notice:first").css({display:"none"}); From 3328ec545c36fc2408cdb9d048effe24feafe218 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 11:27:57 -0700 Subject: [PATCH 63/83] make profile notice getting use ids --- classes/Notice.php | 6 +++--- classes/Profile.php | 51 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 808631f4dc..eceed325b0 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -363,10 +363,10 @@ class Notice extends Memcached_DataObject { if ($this->is_local) { $cache = common_memcache(); - if ($cache) { - $cache->delete(common_cache_key('profile:notices:'.$this->profile_id)); + if (!empty($cache)) { + $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last')); + $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last')); } } } diff --git a/classes/Profile.php b/classes/Profile.php index f3bfe299cf..ae5641d79d 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -155,14 +155,51 @@ class Profile extends Memcached_DataObject function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - $qry = - 'SELECT * ' . - 'FROM notice ' . - 'WHERE profile_id = %d '; + // XXX: I'm not sure this is going to be any faster. It probably isn't. + $ids = Notice::stream(array($this, '_streamDirect'), + array(), + 'profile:notice_ids:' . $this->id, + $offset, $limit, $since_id, $before_id); - return Notice::getStream(sprintf($qry, $this->id), - 'profile:notices:'.$this->id, - $offset, $limit, $since_id, $before_id); + return Notice::getStreamByIds($ids); + } + + function _streamDirect($offset, $limit, $since_id, $before_id, $since) + { + $notice = new Notice(); + + $notice->profile_id = $this->id; + + $notice->selectAdd(); + $notice->selectAdd('id'); + + if ($since_id != 0) { + $notice->whereAdd('id > ' . $since_id); + } + + if ($before_id != 0) { + $notice->whereAdd('id < ' . $before_id); + } + + if (!is_null($since)) { + $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $notice->orderBy('id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + return $ids; } function isMember($group) From 021b520a11d3449a1476182e1ad117582999d364 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 11:38:50 -0700 Subject: [PATCH 64/83] Make user group stream use IDs --- classes/Notice.php | 4 ++-- classes/User_group.php | 53 +++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index eceed325b0..c036e6e9e5 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -299,9 +299,9 @@ class Notice extends Memcached_DataObject $group_inbox->notice_id = $this->id; if ($group_inbox->find()) { while ($group_inbox->fetch()) { - $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id)); + $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id)); if ($blowLast) { - $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id.';last')); + $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last')); } $member = new Group_member(); $member->group_id = $group_inbox->group_id; diff --git a/classes/User_group.php b/classes/User_group.php index d152f9d567..7cc31e7026 100755 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -50,13 +50,50 @@ class User_group extends Memcached_DataObject function getNotices($offset, $limit) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN group_inbox ON notice.id = group_inbox.notice_id ' . - 'WHERE group_inbox.group_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'group:notices:'.$this->id, - $offset, $limit); + $ids = Notice::stream(array($this, '_streamDirect'), + array(), + 'user_group:notice_ids:' . $this->id, + $offset, $limit); + + return Notice::getStreamByIds($ids); + } + + function _streamDirect($offset, $limit, $since_id, $before_id, $since) + { + $inbox = new Group_inbox(); + + $inbox->group_id = $this->id; + + $inbox->selectAdd(); + $inbox->selectAdd('notice_id'); + + if ($since_id != 0) { + $inbox->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $inbox->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $inbox->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $inbox->limit($offset, $limit); + } + + $ids = array(); + + if ($inbox->find()) { + while ($inbox->fetch()) { + $ids[] = $inbox->notice_id; + } + } + + return $ids; } function allowedNickname($nickname) @@ -91,7 +128,7 @@ class User_group extends Memcached_DataObject function setOriginal($filename) { $imagefile = new ImageFile($this->id, Avatar::path($filename)); - + $orig = clone($this); $this->original_logo = Avatar::url($filename); $this->homepage_logo = Avatar::url($imagefile->resize(AVATAR_PROFILE_SIZE)); From 5314d9b2cfaef3f2fd0ead262e18d1776fd99c8d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 1 May 2009 12:01:28 -0700 Subject: [PATCH 65/83] make faves work with ids --- classes/Fave.php | 53 ++++++++++++++++++++++++++++++++++++++++++++-- classes/Notice.php | 4 ++-- classes/User.php | 38 +++++++++++++++------------------ 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/classes/Fave.php b/classes/Fave.php index 24df5938c2..915b4572ff 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -4,7 +4,7 @@ */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Fave extends Memcached_DataObject +class Fave extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -31,9 +31,58 @@ class Fave extends Memcached_DataObject } return $fave; } - + function &pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Fave', $kv); } + + function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE) + { + $ids = Notice::stream(array('Fave', '_streamDirect'), + array($user_id), + 'fave:ids_by_user:'.$user_id, + $offset, $limit); + return $ids; + } + + function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since) + { + $fav = new Fave(); + + $fav->user_id = $user_id; + + $fav->selectAdd(); + $fav->selectAdd('notice_id'); + + if ($since_id != 0) { + $fav->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $fav->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $fav->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + // NOTE: we sort by fave time, not by notice time! + + $fav->orderBy('modified DESC'); + + if (!is_null($offset)) { + $fav->limit($offset, $limit); + } + + $ids = array(); + + if ($fav->find()) { + while ($fav->fetch()) { + $ids[] = $fav->notice_id; + } + } + + return $ids; + } } diff --git a/classes/Notice.php b/classes/Notice.php index c036e6e9e5..771a4e715f 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -412,9 +412,9 @@ class Notice extends Memcached_DataObject $fave->notice_id = $this->id; if ($fave->find()) { while ($fave->fetch()) { - $cache->delete(common_cache_key('user:faves:'.$fave->user_id)); + $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last')); + $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last')); } } } diff --git a/classes/User.php b/classes/User.php index b76e45c330..b5ac7b2206 100644 --- a/classes/User.php +++ b/classes/User.php @@ -349,30 +349,31 @@ class User extends Memcached_DataObject $cache = common_memcache(); // XXX: Kind of a hack. + if ($cache) { // This is the stream of favorite notices, in rev chron // order. This forces it into cache. - $faves = $this->favoriteNotices(0, NOTICE_CACHE_WINDOW); - $cnt = 0; - while ($faves->fetch()) { - if ($faves->id < $notice->id) { - // If we passed it, it's not a fave - return false; - } else if ($faves->id == $notice->id) { - // If it matches a cached notice, then it's a fave - return true; - } - $cnt++; + + $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW); + + // If it's in the list, then it's a fave + + if (in_array($notice->id, $ids)) { + return true; } + // If we're not past the end of the cache window, // then the cache has all available faves, so this one // is not a fave. - if ($cnt < NOTICE_CACHE_WINDOW) { + + if (count($ids) < NOTICE_CACHE_WINDOW) { return false; } + // Otherwise, cache doesn't have all faves; // fall through to the default } + $fave = Fave::pkeyGet(array('user_id' => $this->id, 'notice_id' => $notice->id)); return ((is_null($fave)) ? false : true); @@ -418,13 +419,8 @@ class User extends Memcached_DataObject function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN fave ON notice.id = fave.notice_id ' . - 'WHERE fave.user_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'user:faves:'.$this->id, - $offset, $limit); + $ids = Fave::stream($this->id, $offset, $limit); + return Notice::getStreamByIds($ids); } function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) @@ -459,8 +455,8 @@ class User extends Memcached_DataObject if ($cache) { // Faves don't happen chronologically, so we need to blow // ;last cache, too - $cache->delete(common_cache_key('user:faves:'.$this->id)); - $cache->delete(common_cache_key('user:faves:'.$this->id).';last'); + $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id)); + $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last')); } } From 1fde80cf73c3cf43e3613f7f57fe213df688a1ec Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 1 May 2009 23:32:59 +0000 Subject: [PATCH 66/83] Minor CSS updates (No min-height on shownotice page, site_notice is floated instead of positioned absolutely, notice entry-content is aligned with the nickname on shownotice page) --- theme/base/css/display.css | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index efa5f4ac69..c242977a9f 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -248,10 +248,10 @@ display:none; } #site_notice { -position:absolute; -top:65px; -right:18px; -width:250px; +float:right; +clear:right; +margin-top:7px; +margin-right:18px; width:24%; } #page_notice { @@ -397,6 +397,9 @@ border-radius:7px; border-style:solid; border-width:1px; } +#shownotice #content { +min-height:0; +} #content_inner { position:relative; @@ -812,11 +815,14 @@ clear:left; float:left; font-size:0.95em; margin-left:59px; -width:70%; +width:65%; } #showstream .notice div.entry-content { margin-left:0; } +#shownotice .notice div.entry-content { +margin-left:108px; +} .notice .notice-options a, .notice .notice-options input { From 51377258a1660fee02a3f92436dc40ee40f2a88d Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 2 May 2009 00:10:50 +0000 Subject: [PATCH 67/83] Aligned shownotice page's entry-content to left. Fixing vcard photo margin bottom value. --- theme/base/css/display.css | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index c242977a9f..10fc636385 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -769,16 +769,14 @@ overflow:hidden; font-weight:bold; } -.notice .author .photo { -margin-bottom:0; -} - .vcard .photo { display:inline; margin-right:11px; -margin-bottom:11px; float:left; } +#shownotice .vcard .photo { +margin-bottom:4px; +} .vcard .url { text-decoration:none; } @@ -817,11 +815,9 @@ font-size:0.95em; margin-left:59px; width:65%; } -#showstream .notice div.entry-content { -margin-left:0; -} +#showstream .notice div.entry-content, #shownotice .notice div.entry-content { -margin-left:108px; +margin-left:0; } .notice .notice-options a, From 6a12598695637e7ebdc613977c797413957cc464 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 3 May 2009 21:36:03 -0700 Subject: [PATCH 68/83] add pingvine notice source --- db/notice_source.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/db/notice_source.sql b/db/notice_source.sql index ce44f32354..17720028dc 100644 --- a/db/notice_source.sql +++ b/db/notice_source.sql @@ -24,6 +24,7 @@ VALUES ('peoplebrowsr', 'PeopleBrowsr', 'http://www.peoplebrowsr.com/', now()), ('Pikchur','Pikchur','http://www.pikchur.com/', now()), ('Ping.fm','Ping.fm','http://ping.fm/', now()), + ('pingvine','PingVine','http://pingvine.com/', now()), ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()), ('posty','Posty','http://spreadingfunkyness.com/posty/', now()), ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()), From 7f417cfee023b372a281f5e45a7593c09e279233 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 May 2009 19:28:57 +0000 Subject: [PATCH 69/83] More work on 2-way Twitter sync. Works better now with lastest version of DB_DataObject that automatically reconnects to the DB, but forked processes still lose connections occassionally. --- scripts/statusfetcher.php | 135 +++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php index 8f4b60cf74..8c3ee4330c 100644 --- a/scripts/statusfetcher.php +++ b/scripts/statusfetcher.php @@ -27,41 +27,20 @@ if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); define('LACONICA', true); +// Tune number of processes and how often to poll Twitter +define('MAXCHILDREN', 5); +define('POLL_INTERVAL', 60 * 5); // in seconds + // Uncomment this to get useful console output define('SCRIPT_DEBUG', true); require_once(INSTALLDIR . '/lib/common.php'); $children = array(); -$flink_ids = null; - -$MAXCHILDREN = 5; -$POLL_INTERVAL = 10; // 10 seconds do { - $flink = new Foreign_link(); - $flink->service = 1; // Twitter - $cnt = $flink->find(); - - if (defined('SCRIPT_DEBUG')) { - print "Updating Twitter friends subscriptions for $cnt users.\n"; - } - - $flink_ids = array(); - - // XXX: This only reliably happens once. After the first interation of - // the do loop, the ->find() doesn't work ... lost DB connection? - - while ($flink->fetch()) { - - if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) { - $flink_ids[] = $flink->foreign_id; - } - } - - $flink->free(); - unset($flink); + $flink_ids = refreshFlinks(); foreach ($flink_ids as $f){ @@ -82,7 +61,6 @@ do { // Child // XXX: Each child needs its own DB connection - getTimeline($f); exit(); } @@ -96,11 +74,11 @@ do { } // Wait if we have too many kids - if(sizeof($children) > $MAXCHILDREN) { + if(sizeof($children) > MAXCHILDREN) { if (defined('SCRIPT_DEBUG')) { print "Too many children. Waiting...\n"; } - if( ($c = pcntl_wait($status, WUNTRACED) ) > 0){ + if(($c = pcntl_wait($status, WUNTRACED)) > 0){ if (defined('SCRIPT_DEBUG')) { print "Finished waiting for $c\n"; } @@ -119,14 +97,44 @@ do { // Rest for a bit before we fetch more statuses if (defined('SCRIPT_DEBUG')) { - print "Waiting $POLL_INTERVAL secs before hitting Twitter again.\n"; + print 'Waiting ' . POLL_INTERVAL . + " secs before hitting Twitter again.\n"; } - sleep($POLL_INTERVAL); + sleep(POLL_INTERVAL); } while (true); +function refreshFlinks() { + + global $config; + + $flink = new Foreign_link(); + $flink->service = 1; // Twitter + $flink->orderBy('last_noticesync'); + + $cnt = $flink->find(); + + if (defined('SCRIPT_DEBUG')) { + print "Updating Twitter friends subscriptions for $cnt users.\n"; + } + + $flinks = array(); + + while ($flink->fetch()) { + + if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) { + $flinks[] = clone($flink); + } + } + + $flink->free(); + unset($flink); + + return $flinks; +} + function remove_ps(&$plist, $ps){ for($i = 0; $i < sizeof($plist); $i++){ if($plist[$i] == $ps){ @@ -137,22 +145,43 @@ function remove_ps(&$plist, $ps){ } } -function getTimeline($fid) +function getTimeline($flink) { - // XXX: Need to reconnect to the DB here? + global $config; + $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); + require_once(INSTALLDIR . '/lib/common.php'); - $flink = Foreign_link::getByForeignID($fid, 1); - $fuser = $flink->getForeignUser(); + if (defined('SCRIPT_DEBUG')) { + print "Trying to get timeline for $flink->foreign_id\n"; + } + + if (empty($flink)) { + common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); + if (defined('SCRIPT_DEBUG')) { + print "Can't retrieve Foreign_link for foreign ID $fid\n"; + } + return; + } + + $fuser = new Foreign_user(); + $fuser->service = 1; + $fuser->id = $flink->foreign_id; + $fuser->limit(1); + $fuser->find(true); if (empty($fuser)) { common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); if (defined('SCRIPT_DEBUG')) { print "Unmatched user for ID $flink->user_id\n"; } + return; } - $screenname = $fuser->nickname; + if (defined('SCRIPT_DEBUG')) { + // XXX: This is horrible and must be removed before releasing this + print 'username: ' . $fuser->nickname . ' password: ' . $flink->credentials . "\n"; + } $url = 'http://twitter.com/statuses/friends_timeline.json'; @@ -181,10 +210,19 @@ function getTimeline($fid) saveStatus($status, $flink); } + // Okay, record the time we synced with Twitter for posterity + + $flink->last_noticesync = common_sql_now(); + $flink->update(); } function saveStatus($status, $flink) { + + global $config; + $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); + require_once(INSTALLDIR . '/lib/common.php'); + // Do we have a profile for this Twitter user? $id = ensureProfile($status->user); @@ -244,6 +282,9 @@ function saveStatus($status, $flink) $notice->query('COMMIT'); + if (defined('SCRIPT_DEBUG')) { + print "Saved status $status->id as notice $notice->id.\n"; + } } if (!Notice_inbox::staticGet('notice_id', $notice->id)) { @@ -260,6 +301,7 @@ function saveStatus($status, $flink) function ensureProfile($user) { + global $config; // check to see if there's already a profile for this user $profileurl = 'http://twitter.com/' . $user->screen_name; @@ -328,8 +370,6 @@ function ensureProfile($user) } $profile->query("COMMIT"); - $profile->free(); - unset($profile); saveAvatars($user, $id); @@ -339,6 +379,8 @@ function ensureProfile($user) function checkAvatar($user, $profile) { + global $config; + $path_parts = pathinfo($user->profile_image_url); $newname = 'Twitter_' . $user->id . '_' . $path_parts['basename']; @@ -393,6 +435,8 @@ function getMediatype($ext) function saveAvatars($user, $id) { + global $config; + $path_parts = pathinfo($user->profile_image_url); $ext = $path_parts['extension']; $end = strlen('_normal' . $ext); @@ -418,7 +462,12 @@ function saveAvatars($user, $id) function updateAvatar($profile_id, $size, $mediatype, $filename) { - common_debug("updating avatar: $size"); + global $config; + + common_debug("Updating avatar: $size"); + if (defined('SCRIPT_DEBUG')) { + print "Updating avatar: $size\n"; + } $profile = Profile::staticGet($profile_id); @@ -444,6 +493,8 @@ function updateAvatar($profile_id, $size, $mediatype, $filename) { function newAvatar($profile_id, $size, $mediatype, $filename) { + global $config; + $avatar = new Avatar(); $avatar->profile_id = $profile_id; @@ -471,6 +522,9 @@ function newAvatar($profile_id, $size, $mediatype, $filename) $avatar->url = Avatar::url($filename); common_debug("new filename: $avatar->url"); + if (defined('SCRIPT_DEBUG')) { + print "New filename: $avatar->url\n"; + } $avatar->created = common_sql_now(); @@ -486,6 +540,9 @@ function newAvatar($profile_id, $size, $mediatype, $filename) } common_debug("Saved new $size avatar for $profile_id."); + if (defined('SCRIPT_DEBUG')) { + print "Saved new $size avatar for $profile_id.\n"; + } return $id; } From 99e8f3235f2718f46cc95966ae39e725ee31a7df Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 6 May 2009 01:12:26 +0000 Subject: [PATCH 70/83] This finally works (provided the newer version of DB_DataObject that auto-reconnects to the DB). --- scripts/statusfetcher.php | 87 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php index 8c3ee4330c..82ae5bfd43 100644 --- a/scripts/statusfetcher.php +++ b/scripts/statusfetcher.php @@ -29,7 +29,11 @@ define('LACONICA', true); // Tune number of processes and how often to poll Twitter define('MAXCHILDREN', 5); +<<<<<<< HEAD:scripts/statusfetcher.php +define('POLL_INTERVAL', 60 * 10); // in seconds +======= define('POLL_INTERVAL', 60 * 5); // in seconds +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php // Uncomment this to get useful console output define('SCRIPT_DEBUG', true); @@ -40,9 +44,20 @@ $children = array(); do { - $flink_ids = refreshFlinks(); +<<<<<<< HEAD:scripts/statusfetcher.php + $flinks = refreshFlinks(); - foreach ($flink_ids as $f){ + foreach ($flinks as $f){ + + // We have to disconnect from the DB before forking so + // each process will open its own connection and + // avoid stomping on each other +======= + $flink_ids = refreshFlinks(); +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php + + $conn = &$f->getDatabaseConnection(); + $conn->disconnect(); $pid = pcntl_fork(); @@ -50,38 +65,60 @@ do { die ("Couldn't fork!"); } - // Parent if ($pid) { + + // Parent + if (defined('SCRIPT_DEBUG')) { print "Parent: forked " . $pid . "\n"; } + $children[] = $pid; + } else { // Child +<<<<<<< HEAD:scripts/statusfetcher.php + getTimeline($f, $child_db_name); +======= // XXX: Each child needs its own DB connection getTimeline($f); +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php exit(); } // Remove child from ps list as it finishes while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { + if (defined('SCRIPT_DEBUG')) { print "Child $c finished.\n"; } + remove_ps($children, $c); } // Wait if we have too many kids +<<<<<<< HEAD:scripts/statusfetcher.php + if (sizeof($children) > MAXCHILDREN) { + + if (defined('SCRIPT_DEBUG')) { + print "Too many children. Waiting...\n"; + } + + if (($c = pcntl_wait($status, WUNTRACED)) > 0){ + +======= if(sizeof($children) > MAXCHILDREN) { if (defined('SCRIPT_DEBUG')) { print "Too many children. Waiting...\n"; } if(($c = pcntl_wait($status, WUNTRACED)) > 0){ +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (defined('SCRIPT_DEBUG')) { print "Finished waiting for $c\n"; } + remove_ps($children, $c); } } @@ -89,13 +126,17 @@ do { // Remove all children from the process list before restarting while(($c = pcntl_wait($status, WUNTRACED)) > 0) { + if (defined('SCRIPT_DEBUG')) { print "Child $c finished.\n"; } + remove_ps($children, $c); } // Rest for a bit before we fetch more statuses + common_debug('Waiting ' . POLL_INTERVAL . + ' secs before hitting Twitter again.'); if (defined('SCRIPT_DEBUG')) { print 'Waiting ' . POLL_INTERVAL . " secs before hitting Twitter again.\n"; @@ -108,8 +149,11 @@ do { function refreshFlinks() { +<<<<<<< HEAD:scripts/statusfetcher.php +======= global $config; +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php $flink = new Foreign_link(); $flink->service = 1; // Twitter $flink->orderBy('last_noticesync'); @@ -136,8 +180,8 @@ function refreshFlinks() { } function remove_ps(&$plist, $ps){ - for($i = 0; $i < sizeof($plist); $i++){ - if($plist[$i] == $ps){ + for ($i = 0; $i < sizeof($plist); $i++) { + if ($plist[$i] == $ps) { unset($plist[$i]); $plist = array_values($plist); break; @@ -148,6 +192,17 @@ function remove_ps(&$plist, $ps){ function getTimeline($flink) { +<<<<<<< HEAD:scripts/statusfetcher.php + if (empty($flink)) { + common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); + if (defined('SCRIPT_DEBUG')) { + print "Can't retrieve Foreign_link for foreign ID $fid\n"; + } + return; + } + + $fuser = $flink->getForeignUser(); +======= global $config; $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); require_once(INSTALLDIR . '/lib/common.php'); @@ -169,6 +224,7 @@ function getTimeline($flink) $fuser->id = $flink->foreign_id; $fuser->limit(1); $fuser->find(true); +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (empty($fuser)) { common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); @@ -178,9 +234,17 @@ function getTimeline($flink) return; } +<<<<<<< HEAD:scripts/statusfetcher.php + common_debug('Trying to get timeline for Twitter user ' . + "$fuser->nickname ($flink->foreign_id)."); + if (defined('SCRIPT_DEBUG')) { + print 'Trying to get timeline for Twitter user ' . + "$fuser->nickname ($flink->foreign_id).\n"; +======= if (defined('SCRIPT_DEBUG')) { // XXX: This is horrible and must be removed before releasing this print 'username: ' . $fuser->nickname . ' password: ' . $flink->credentials . "\n"; +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php } $url = 'http://twitter.com/statuses/friends_timeline.json'; @@ -218,6 +282,8 @@ function getTimeline($flink) function saveStatus($status, $flink) { +<<<<<<< HEAD:scripts/statusfetcher.php +======= global $config; $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); @@ -225,6 +291,7 @@ function saveStatus($status, $flink) // Do we have a profile for this Twitter user? +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php $id = ensureProfile($status->user); $profile = Profile::staticGet($id); @@ -282,6 +349,10 @@ function saveStatus($status, $flink) $notice->query('COMMIT'); +<<<<<<< HEAD:scripts/statusfetcher.php + common_debug("Saved status $status->id as notice $notice->id."); +======= +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (defined('SCRIPT_DEBUG')) { print "Saved status $status->id as notice $notice->id.\n"; } @@ -301,8 +372,11 @@ function saveStatus($status, $flink) function ensureProfile($user) { +<<<<<<< HEAD:scripts/statusfetcher.php +======= global $config; +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php // check to see if there's already a profile for this user $profileurl = 'http://twitter.com/' . $user->screen_name; $profile = Profile::staticGet('profileurl', $profileurl); @@ -462,8 +536,11 @@ function saveAvatars($user, $id) function updateAvatar($profile_id, $size, $mediatype, $filename) { +<<<<<<< HEAD:scripts/statusfetcher.php +======= global $config; +>>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php common_debug("Updating avatar: $size"); if (defined('SCRIPT_DEBUG')) { print "Updating avatar: $size\n"; From b291cb8a1be0eb272e1663fbf6c6dea17bdb71db Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 6 May 2009 01:26:06 +0000 Subject: [PATCH 71/83] Fix for previous bad patch I pushed (had conflict markers) Sorry about that. --- scripts/statusfetcher.php | 80 --------------------------------------- 1 file changed, 80 deletions(-) diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php index 82ae5bfd43..5518e3aa8c 100644 --- a/scripts/statusfetcher.php +++ b/scripts/statusfetcher.php @@ -29,11 +29,7 @@ define('LACONICA', true); // Tune number of processes and how often to poll Twitter define('MAXCHILDREN', 5); -<<<<<<< HEAD:scripts/statusfetcher.php define('POLL_INTERVAL', 60 * 10); // in seconds -======= -define('POLL_INTERVAL', 60 * 5); // in seconds ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php // Uncomment this to get useful console output define('SCRIPT_DEBUG', true); @@ -44,7 +40,6 @@ $children = array(); do { -<<<<<<< HEAD:scripts/statusfetcher.php $flinks = refreshFlinks(); foreach ($flinks as $f){ @@ -52,9 +47,6 @@ do { // We have to disconnect from the DB before forking so // each process will open its own connection and // avoid stomping on each other -======= - $flink_ids = refreshFlinks(); ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php $conn = &$f->getDatabaseConnection(); $conn->disconnect(); @@ -79,12 +71,7 @@ do { // Child -<<<<<<< HEAD:scripts/statusfetcher.php getTimeline($f, $child_db_name); -======= - // XXX: Each child needs its own DB connection - getTimeline($f); ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php exit(); } @@ -99,7 +86,6 @@ do { } // Wait if we have too many kids -<<<<<<< HEAD:scripts/statusfetcher.php if (sizeof($children) > MAXCHILDREN) { if (defined('SCRIPT_DEBUG')) { @@ -108,13 +94,6 @@ do { if (($c = pcntl_wait($status, WUNTRACED)) > 0){ -======= - if(sizeof($children) > MAXCHILDREN) { - if (defined('SCRIPT_DEBUG')) { - print "Too many children. Waiting...\n"; - } - if(($c = pcntl_wait($status, WUNTRACED)) > 0){ ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (defined('SCRIPT_DEBUG')) { print "Finished waiting for $c\n"; } @@ -149,11 +128,6 @@ do { function refreshFlinks() { -<<<<<<< HEAD:scripts/statusfetcher.php -======= - global $config; - ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php $flink = new Foreign_link(); $flink->service = 1; // Twitter $flink->orderBy('last_noticesync'); @@ -192,7 +166,6 @@ function remove_ps(&$plist, $ps){ function getTimeline($flink) { -<<<<<<< HEAD:scripts/statusfetcher.php if (empty($flink)) { common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); if (defined('SCRIPT_DEBUG')) { @@ -202,29 +175,6 @@ function getTimeline($flink) } $fuser = $flink->getForeignUser(); -======= - global $config; - $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); - require_once(INSTALLDIR . '/lib/common.php'); - - if (defined('SCRIPT_DEBUG')) { - print "Trying to get timeline for $flink->foreign_id\n"; - } - - if (empty($flink)) { - common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); - if (defined('SCRIPT_DEBUG')) { - print "Can't retrieve Foreign_link for foreign ID $fid\n"; - } - return; - } - - $fuser = new Foreign_user(); - $fuser->service = 1; - $fuser->id = $flink->foreign_id; - $fuser->limit(1); - $fuser->find(true); ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (empty($fuser)) { common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); @@ -234,17 +184,11 @@ function getTimeline($flink) return; } -<<<<<<< HEAD:scripts/statusfetcher.php common_debug('Trying to get timeline for Twitter user ' . "$fuser->nickname ($flink->foreign_id)."); if (defined('SCRIPT_DEBUG')) { print 'Trying to get timeline for Twitter user ' . "$fuser->nickname ($flink->foreign_id).\n"; -======= - if (defined('SCRIPT_DEBUG')) { - // XXX: This is horrible and must be removed before releasing this - print 'username: ' . $fuser->nickname . ' password: ' . $flink->credentials . "\n"; ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php } $url = 'http://twitter.com/statuses/friends_timeline.json'; @@ -282,16 +226,6 @@ function getTimeline($flink) function saveStatus($status, $flink) { -<<<<<<< HEAD:scripts/statusfetcher.php -======= - - global $config; - $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); - require_once(INSTALLDIR . '/lib/common.php'); - - // Do we have a profile for this Twitter user? - ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php $id = ensureProfile($status->user); $profile = Profile::staticGet($id); @@ -349,10 +283,6 @@ function saveStatus($status, $flink) $notice->query('COMMIT'); -<<<<<<< HEAD:scripts/statusfetcher.php - common_debug("Saved status $status->id as notice $notice->id."); -======= ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php if (defined('SCRIPT_DEBUG')) { print "Saved status $status->id as notice $notice->id.\n"; } @@ -372,11 +302,6 @@ function saveStatus($status, $flink) function ensureProfile($user) { -<<<<<<< HEAD:scripts/statusfetcher.php -======= - global $config; - ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php // check to see if there's already a profile for this user $profileurl = 'http://twitter.com/' . $user->screen_name; $profile = Profile::staticGet('profileurl', $profileurl); @@ -536,11 +461,6 @@ function saveAvatars($user, $id) function updateAvatar($profile_id, $size, $mediatype, $filename) { -<<<<<<< HEAD:scripts/statusfetcher.php -======= - global $config; - ->>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php common_debug("Updating avatar: $size"); if (defined('SCRIPT_DEBUG')) { print "Updating avatar: $size\n"; From 48226e0c48e9bb2a7d97dbfd8f048ae299fbb7bf Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 00:25:15 -0700 Subject: [PATCH 72/83] Properly daemonized 2-way Twitter bridge code --- scripts/statusfetcher.php | 929 ++++++++++++++++++++------------------ 1 file changed, 480 insertions(+), 449 deletions(-) diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php index 5518e3aa8c..5275a45752 100644 --- a/scripts/statusfetcher.php +++ b/scripts/statusfetcher.php @@ -28,378 +28,439 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); define('LACONICA', true); // Tune number of processes and how often to poll Twitter -define('MAXCHILDREN', 5); -define('POLL_INTERVAL', 60 * 10); // in seconds +// XXX: Should these things be in config.php? +define('MAXCHILDREN', 2); +define('POLL_INTERVAL', 60); // in seconds // Uncomment this to get useful console output define('SCRIPT_DEBUG', true); require_once(INSTALLDIR . '/lib/common.php'); +require_once(INSTALLDIR . '/lib/daemon.php'); -$children = array(); +class TwitterStatusFetcher extends Daemon +{ -do { + private $children = array(); - $flinks = refreshFlinks(); + function name() + { + return 'twitterstatusfetcher'; + } - foreach ($flinks as $f){ + function run() + { + do { - // We have to disconnect from the DB before forking so - // each process will open its own connection and - // avoid stomping on each other + $flinks = $this->refreshFlinks(); - $conn = &$f->getDatabaseConnection(); - $conn->disconnect(); + foreach ($flinks as $f){ - $pid = pcntl_fork(); + // We have to disconnect from the DB before forking so + // each sub-process will open its own connection and + // avoid stomping on the others - if ($pid == -1) { - die ("Couldn't fork!"); - } + $conn = &$f->getDatabaseConnection(); + $conn->disconnect(); - if ($pid) { + $pid = pcntl_fork(); - // Parent - - if (defined('SCRIPT_DEBUG')) { - print "Parent: forked " . $pid . "\n"; - } - - $children[] = $pid; - - } else { - - // Child - - getTimeline($f, $child_db_name); - exit(); - } - - // Remove child from ps list as it finishes - while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - print "Child $c finished.\n"; - } - - remove_ps($children, $c); - } - - // Wait if we have too many kids - if (sizeof($children) > MAXCHILDREN) { - - if (defined('SCRIPT_DEBUG')) { - print "Too many children. Waiting...\n"; - } - - if (($c = pcntl_wait($status, WUNTRACED)) > 0){ - - if (defined('SCRIPT_DEBUG')) { - print "Finished waiting for $c\n"; + if ($pid == -1) { + die ("Couldn't fork!"); } - remove_ps($children, $c); + if ($pid) { + + // Parent + common_debug("Parent: forked new status fetcher process " . $pid); + + if (defined('SCRIPT_DEBUG')) { + print "Parent: forked fetcher process " . $pid . "\n"; + } + + $this->children[] = $pid; + + } else { + + // Child + $this->getTimeline($f); + exit(); + } + + // Remove child from ps list as it finishes + while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { + + common_debug("Child $c finished."); + + if (defined('SCRIPT_DEBUG')) { + print "Child $c finished.\n"; + } + + $this->remove_ps($this->children, $c); + } + + // Wait! We have too many damn kids. + if (sizeof($this->children) > MAXCHILDREN) { + + common_debug('Too many children. Waiting...'); + + if (defined('SCRIPT_DEBUG')) { + print "Too many children. Waiting...\n"; + } + + if (($c = pcntl_wait($status, WUNTRACED)) > 0){ + + common_debug("Finished waiting for $c"); + + if (defined('SCRIPT_DEBUG')) { + print "Finished waiting for $c\n"; + } + + $this->remove_ps($this->children, $c); + } + } } - } - } - // Remove all children from the process list before restarting - while(($c = pcntl_wait($status, WUNTRACED)) > 0) { + // Remove all children from the process list before restarting + while(($c = pcntl_wait($status, WUNTRACED)) > 0) { - if (defined('SCRIPT_DEBUG')) { - print "Child $c finished.\n"; - } + common_debug("Child $c finished."); - remove_ps($children, $c); - } + if (defined('SCRIPT_DEBUG')) { + print "Child $c finished.\n"; + } - // Rest for a bit before we fetch more statuses - common_debug('Waiting ' . POLL_INTERVAL . - ' secs before hitting Twitter again.'); - if (defined('SCRIPT_DEBUG')) { - print 'Waiting ' . POLL_INTERVAL . - " secs before hitting Twitter again.\n"; - } + $this->remove_ps($this->children, $c); + } - sleep(POLL_INTERVAL); - -} while (true); - - -function refreshFlinks() { - - $flink = new Foreign_link(); - $flink->service = 1; // Twitter - $flink->orderBy('last_noticesync'); - - $cnt = $flink->find(); - - if (defined('SCRIPT_DEBUG')) { - print "Updating Twitter friends subscriptions for $cnt users.\n"; - } - - $flinks = array(); - - while ($flink->fetch()) { - - if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) { - $flinks[] = clone($flink); - } - } - - $flink->free(); - unset($flink); - - return $flinks; -} - -function remove_ps(&$plist, $ps){ - for ($i = 0; $i < sizeof($plist); $i++) { - if ($plist[$i] == $ps) { - unset($plist[$i]); - $plist = array_values($plist); - break; - } - } -} - -function getTimeline($flink) -{ - - if (empty($flink)) { - common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); - if (defined('SCRIPT_DEBUG')) { - print "Can't retrieve Foreign_link for foreign ID $fid\n"; - } - return; - } - - $fuser = $flink->getForeignUser(); - - if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - if (defined('SCRIPT_DEBUG')) { - print "Unmatched user for ID $flink->user_id\n"; - } - return; - } - - common_debug('Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id)."); - if (defined('SCRIPT_DEBUG')) { - print 'Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id).\n"; - } - - $url = 'http://twitter.com/statuses/friends_timeline.json'; - - $timeline_json = get_twitter_data($url, $fuser->nickname, - $flink->credentials); - - $timeline = json_decode($timeline_json); - - if (empty($timeline)) { - common_log(LOG_WARNING, "Empty timeline."); - if (defined('SCRIPT_DEBUG')) { - print "Empty timeline!\n"; - } - return; - } - - foreach ($timeline as $status) { - - // Hacktastic: filter out stuff coming from Laconica - $source = mb_strtolower(common_config('integration', 'source')); - - if (preg_match("/$source/", mb_strtolower($status->source))) { - continue; - } - - saveStatus($status, $flink); - } - - // Okay, record the time we synced with Twitter for posterity - - $flink->last_noticesync = common_sql_now(); - $flink->update(); -} - -function saveStatus($status, $flink) -{ - $id = ensureProfile($status->user); - $profile = Profile::staticGet($id); - - if (!$profile) { - common_log(LOG_ERR, 'Problem saving notice. No associated Profile.'); - if (defined('SCRIPT_DEBUG')) { - print "Problem saving notice. No associated Profile.\n"; - } - return null; - } - - $uri = 'http://twitter.com/' . $status->user->screen_name . - '/status/' . $status->id; - - // Skip save if notice source is Laconica or Identi.ca? - - $notice = Notice::staticGet('uri', $uri); - - // check to see if we've already imported the status - if (!$notice) { - - $notice = new Notice(); - $notice->profile_id = $id; - - $notice->query('BEGIN'); - - // XXX: figure out reply_to - $notice->reply_to = null; - - // XXX: Should this be common_sql_now() instead of status create date? - - $notice->created = strftime('%Y-%m-%d %H:%M:%S', - strtotime($status->created_at)); - $notice->content = $status->text; - $notice->rendered = common_render_content($status->text, $notice); - $notice->source = 'twitter'; - $notice->is_local = 0; - $notice->uri = $uri; - - $notice_id = $notice->insert(); - - if (!$notice_id) { - common_log_db_error($notice, 'INSERT', __FILE__); + // Rest for a bit before we fetch more statuses + common_debug('Waiting ' . POLL_INTERVAL . + ' secs before hitting Twitter again.'); if (defined('SCRIPT_DEBUG')) { - print "Could not save notice!\n"; + print 'Waiting ' . POLL_INTERVAL . + " secs before hitting Twitter again.\n"; + } + + sleep(POLL_INTERVAL); + + } while (true); + } + + function refreshFlinks() { + + $flink = new Foreign_link(); + $flink->service = 1; // Twitter + $flink->orderBy('last_noticesync'); + + $cnt = $flink->find(); + + if (defined('SCRIPT_DEBUG')) { + print "Updating Twitter friends subscriptions for $cnt users.\n"; + } + + $flinks = array(); + + while ($flink->fetch()) { + + if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) { + $flinks[] = clone($flink); } } - // XXX: Figure out a better way to link replies? - $notice->saveReplies(); + $flink->free(); + unset($flink); - // XXX: Do we want to polute our tag cloud with hashtags from Twitter? - $notice->saveTags(); - $notice->saveGroups(); + return $flinks; + } - $notice->query('COMMIT'); - - if (defined('SCRIPT_DEBUG')) { - print "Saved status $status->id as notice $notice->id.\n"; + function remove_ps(&$plist, $ps){ + for ($i = 0; $i < sizeof($plist); $i++) { + if ($plist[$i] == $ps) { + unset($plist[$i]); + $plist = array_values($plist); + break; + } } } - if (!Notice_inbox::staticGet('notice_id', $notice->id)) { + function getTimeline($flink) + { - // Add to inbox - $inbox = new Notice_inbox(); - $inbox->user_id = $flink->user_id; - $inbox->notice_id = $notice->id; - $inbox->created = common_sql_now(); - - $inbox->insert(); - } -} - -function ensureProfile($user) -{ - // check to see if there's already a profile for this user - $profileurl = 'http://twitter.com/' . $user->screen_name; - $profile = Profile::staticGet('profileurl', $profileurl); - - if ($profile) { - common_debug("Profile for $profile->nickname found."); - - // Check to see if the user's Avatar has changed - checkAvatar($user, $profile); - return $profile->id; - - } else { - $debugmsg = 'Adding profile and remote profile ' . - "for Twitter user: $profileurl\n"; - common_debug($debugmsg, __FILE__); - if (defined('SCRIPT_DEBUG')) { - print $debugmsg; - } - - $profile = new Profile(); - $profile->query("BEGIN"); - - $profile->nickname = $user->screen_name; - $profile->fullname = $user->name; - $profile->homepage = $user->url; - $profile->bio = $user->description; - $profile->location = $user->location; - $profile->profileurl = $profileurl; - $profile->created = common_sql_now(); - - $id = $profile->insert(); - - if (empty($id)) { - common_log_db_error($profile, 'INSERT', __FILE__); + if (empty($flink)) { + common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); if (defined('SCRIPT_DEBUG')) { - print 'Could not insert Profile: ' . - common_log_objstring($profile) . "\n"; + print "Can't retrieve Foreign_link for foreign ID $fid\n"; } - $profile->query("ROLLBACK"); - return false; + return; } - // check for remote profile - $remote_pro = Remote_profile::staticGet('uri', $profileurl); + $fuser = $flink->getForeignUser(); - if (!$remote_pro) { + if (empty($fuser)) { + common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); + if (defined('SCRIPT_DEBUG')) { + print "Unmatched user for ID $flink->user_id\n"; + } + return; + } - $remote_pro = new Remote_profile(); + common_debug('Trying to get timeline for Twitter user ' . + "$fuser->nickname ($flink->foreign_id)."); + if (defined('SCRIPT_DEBUG')) { + print 'Trying to get timeline for Twitter user ' . + "$fuser->nickname ($flink->foreign_id).\n"; + } - $remote_pro->id = $id; - $remote_pro->uri = $profileurl; - $remote_pro->created = common_sql_now(); + $url = 'http://twitter.com/statuses/friends_timeline.json'; - $rid = $remote_pro->insert(); + $timeline_json = get_twitter_data($url, $fuser->nickname, + $flink->credentials); - if (empty($rid)) { + $timeline = json_decode($timeline_json); + + if (empty($timeline)) { + common_log(LOG_WARNING, "Empty timeline."); + if (defined('SCRIPT_DEBUG')) { + print "Empty timeline!\n"; + } + return; + } + + foreach ($timeline as $status) { + + // Hacktastic: filter out stuff coming from Laconica + $source = mb_strtolower(common_config('integration', 'source')); + + if (preg_match("/$source/", mb_strtolower($status->source))) { + continue; + } + + $this->saveStatus($status, $flink); + } + + // Okay, record the time we synced with Twitter for posterity + + $flink->last_noticesync = common_sql_now(); + $flink->update(); + } + + function saveStatus($status, $flink) + { + $id = $this->ensureProfile($status->user); + $profile = Profile::staticGet($id); + + if (!$profile) { + common_log(LOG_ERR, 'Problem saving notice. No associated Profile.'); + if (defined('SCRIPT_DEBUG')) { + print "Problem saving notice. No associated Profile.\n"; + } + return null; + } + + $uri = 'http://twitter.com/' . $status->user->screen_name . + '/status/' . $status->id; + + // Skip save if notice source is Laconica or Identi.ca? + + $notice = Notice::staticGet('uri', $uri); + + // check to see if we've already imported the status + if (!$notice) { + + $notice = new Notice(); + $notice->profile_id = $id; + + $notice->query('BEGIN'); + + // XXX: figure out reply_to + $notice->reply_to = null; + + // XXX: Should this be common_sql_now() instead of status create date? + + $notice->created = strftime('%Y-%m-%d %H:%M:%S', + strtotime($status->created_at)); + $notice->content = $status->text; + $notice->rendered = common_render_content($status->text, $notice); + $notice->source = 'twitter'; + $notice->is_local = 0; + $notice->uri = $uri; + + $notice_id = $notice->insert(); + + if (!$notice_id) { + common_log_db_error($notice, 'INSERT', __FILE__); + if (defined('SCRIPT_DEBUG')) { + print "Could not save notice!\n"; + } + } + + // XXX: Figure out a better way to link replies? + $notice->saveReplies(); + + // XXX: Do we want to polute our tag cloud with hashtags from Twitter? + $notice->saveTags(); + $notice->saveGroups(); + + $notice->query('COMMIT'); + + if (defined('SCRIPT_DEBUG')) { + print "Saved status $status->id as notice $notice->id.\n"; + } + } + + if (!Notice_inbox::staticGet('notice_id', $notice->id)) { + + // Add to inbox + $inbox = new Notice_inbox(); + $inbox->user_id = $flink->user_id; + $inbox->notice_id = $notice->id; + $inbox->created = common_sql_now(); + + $inbox->insert(); + } + } + + function ensureProfile($user) + { + // check to see if there's already a profile for this user + $profileurl = 'http://twitter.com/' . $user->screen_name; + $profile = Profile::staticGet('profileurl', $profileurl); + + if ($profile) { + common_debug("Profile for $profile->nickname found."); + + // Check to see if the user's Avatar has changed + $this->checkAvatar($user, $profile); + return $profile->id; + + } else { + $debugmsg = 'Adding profile and remote profile ' . + "for Twitter user: $profileurl\n"; + common_debug($debugmsg, __FILE__); + if (defined('SCRIPT_DEBUG')) { + print $debugmsg; + } + + $profile = new Profile(); + $profile->query("BEGIN"); + + $profile->nickname = $user->screen_name; + $profile->fullname = $user->name; + $profile->homepage = $user->url; + $profile->bio = $user->description; + $profile->location = $user->location; + $profile->profileurl = $profileurl; + $profile->created = common_sql_now(); + + $id = $profile->insert(); + + if (empty($id)) { common_log_db_error($profile, 'INSERT', __FILE__); if (defined('SCRIPT_DEBUG')) { - print 'Could not insert Remote_profile: ' . - common_log_objstring($remote_pro) . "\n"; + print 'Could not insert Profile: ' . + common_log_objstring($profile) . "\n"; } $profile->query("ROLLBACK"); return false; } + + // check for remote profile + $remote_pro = Remote_profile::staticGet('uri', $profileurl); + + if (!$remote_pro) { + + $remote_pro = new Remote_profile(); + + $remote_pro->id = $id; + $remote_pro->uri = $profileurl; + $remote_pro->created = common_sql_now(); + + $rid = $remote_pro->insert(); + + if (empty($rid)) { + common_log_db_error($profile, 'INSERT', __FILE__); + if (defined('SCRIPT_DEBUG')) { + print 'Could not insert Remote_profile: ' . + common_log_objstring($remote_pro) . "\n"; + } + $profile->query("ROLLBACK"); + return false; + } + } + + $profile->query("COMMIT"); + + $this->saveAvatars($user, $id); + + return $id; } - - $profile->query("COMMIT"); - - saveAvatars($user, $id); - - return $id; } -} -function checkAvatar($user, $profile) -{ - global $config; + function checkAvatar($user, $profile) + { + global $config; - $path_parts = pathinfo($user->profile_image_url); - $newname = 'Twitter_' . $user->id . '_' . - $path_parts['basename']; + $path_parts = pathinfo($user->profile_image_url); + $newname = 'Twitter_' . $user->id . '_' . + $path_parts['basename']; - $oldname = $profile->getAvatar(48)->filename; + $oldname = $profile->getAvatar(48)->filename; - if ($newname != $oldname) { + if ($newname != $oldname) { - common_debug("Avatar for Twitter user $profile->nickname has changed."); - common_debug("old: $oldname new: $newname"); + common_debug("Avatar for Twitter user $profile->nickname has changed."); + common_debug("old: $oldname new: $newname"); - if (defined('SCRIPT_DEBUG')) { - print "Avatar for Twitter user $user->id has changed.\n"; - print "old: $oldname\n"; - print "new: $newname\n"; + if (defined('SCRIPT_DEBUG')) { + print "Avatar for Twitter user $user->id has changed.\n"; + print "old: $oldname\n"; + print "new: $newname\n"; + } + + $img_root = substr($path_parts['basename'], 0, -11); + $ext = $path_parts['extension']; + $mediatype = $this->getMediatype($ext); + + foreach (array('mini', 'normal', 'bigger') as $size) { + $url = $path_parts['dirname'] . '/' . + $img_root . '_' . $size . ".$ext"; + $filename = 'Twitter_' . $user->id . '_' . + $img_root . "_$size.$ext"; + + if ($this->fetchAvatar($url, $filename)) { + $this->updateAvatar($profile->id, $size, $mediatype, $filename); + } + } + } + } + + function getMediatype($ext) + { + $mediatype = null; + + switch (strtolower($ext)) { + case 'jpg': + $mediatype = 'image/jpg'; + break; + case 'gif': + $mediatype = 'image/gif'; + break; + default: + $mediatype = 'image/png'; } - $img_root = substr($path_parts['basename'], 0, -11); + return $mediatype; + } + + function saveAvatars($user, $id) + { + global $config; + + $path_parts = pathinfo($user->profile_image_url); $ext = $path_parts['extension']; - $mediatype = getMediatype($ext); + $end = strlen('_normal' . $ext); + $img_root = substr($path_parts['basename'], 0, -($end+1)); + $mediatype = $this->getMediatype($ext); foreach (array('mini', 'normal', 'bigger') as $size) { $url = $path_parts['dirname'] . '/' . @@ -407,173 +468,143 @@ function checkAvatar($user, $profile) $filename = 'Twitter_' . $user->id . '_' . $img_root . "_$size.$ext"; - if (fetchAvatar($url, $filename)) { - updateAvatar($profile->id, $size, $mediatype, $filename); + if ($this->fetchAvatar($url, $filename)) { + $this->newAvatar($id, $size, $mediatype, $filename); + } else { + common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__); + if (defined('SCRIPT_DEBUG')) { + print "Problem fetching Avatar: $url\n"; + } } } } -} -function getMediatype($ext) -{ - $mediatype = null; + function updateAvatar($profile_id, $size, $mediatype, $filename) { - switch (strtolower($ext)) { - case 'jpg': - $mediatype = 'image/jpg'; - break; - case 'gif': - $mediatype = 'image/gif'; - break; - default: - $mediatype = 'image/png'; - } + common_debug("Updating avatar: $size"); + if (defined('SCRIPT_DEBUG')) { + print "Updating avatar: $size\n"; + } - return $mediatype; -} + $profile = Profile::staticGet($profile_id); -function saveAvatars($user, $id) -{ - global $config; - - $path_parts = pathinfo($user->profile_image_url); - $ext = $path_parts['extension']; - $end = strlen('_normal' . $ext); - $img_root = substr($path_parts['basename'], 0, -($end+1)); - $mediatype = getMediatype($ext); - - foreach (array('mini', 'normal', 'bigger') as $size) { - $url = $path_parts['dirname'] . '/' . - $img_root . '_' . $size . ".$ext"; - $filename = 'Twitter_' . $user->id . '_' . - $img_root . "_$size.$ext"; - - if (fetchAvatar($url, $filename)) { - newAvatar($id, $size, $mediatype, $filename); - } else { - common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__); + if (!$profile) { + common_debug("Couldn't get profile: $profile_id!"); if (defined('SCRIPT_DEBUG')) { - print "Problem fetching Avatar: $url\n"; + print "Couldn't get profile: $profile_id!\n"; } + return; } - } -} -function updateAvatar($profile_id, $size, $mediatype, $filename) { + $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73); + $avatar = $profile->getAvatar($sizes[$size]); - common_debug("Updating avatar: $size"); - if (defined('SCRIPT_DEBUG')) { - print "Updating avatar: $size\n"; + if ($avatar) { + common_debug("Deleting $size avatar for $profile->nickname."); + @unlink(INSTALLDIR . '/avatar/' . $avatar->filename); + $avatar->delete(); + } + + $this->newAvatar($profile->id, $size, $mediatype, $filename); } - $profile = Profile::staticGet($profile_id); + function newAvatar($profile_id, $size, $mediatype, $filename) + { + global $config; - if (!$profile) { - common_debug("Couldn't get profile: $profile_id!"); + $avatar = new Avatar(); + $avatar->profile_id = $profile_id; + + switch($size) { + case 'mini': + $avatar->width = 24; + $avatar->height = 24; + break; + case 'normal': + $avatar->width = 48; + $avatar->height = 48; + break; + default: + + // Note: Twitter's big avatars are a different size than + // Laconica's (Laconica's = 96) + + $avatar->width = 73; + $avatar->height = 73; + } + + $avatar->original = 0; // we don't have the original + $avatar->mediatype = $mediatype; + $avatar->filename = $filename; + $avatar->url = Avatar::url($filename); + + common_debug("new filename: $avatar->url"); if (defined('SCRIPT_DEBUG')) { - print "Couldn't get profile: $profile_id!\n"; + print "New filename: $avatar->url\n"; } - return; - } - $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73); - $avatar = $profile->getAvatar($sizes[$size]); + $avatar->created = common_sql_now(); - if ($avatar) { - common_debug("Deleting $size avatar for $profile->nickname."); - @unlink(INSTALLDIR . '/avatar/' . $avatar->filename); - $avatar->delete(); - } + $id = $avatar->insert(); - newAvatar($profile->id, $size, $mediatype, $filename); -} + if (!$id) { + common_log_db_error($avatar, 'INSERT', __FILE__); + if (defined('SCRIPT_DEBUG')) { + print "Could not insert avatar!\n"; + } -function newAvatar($profile_id, $size, $mediatype, $filename) -{ - global $config; + return null; + } - $avatar = new Avatar(); - $avatar->profile_id = $profile_id; - - switch($size) { - case 'mini': - $avatar->width = 24; - $avatar->height = 24; - break; - case 'normal': - $avatar->width = 48; - $avatar->height = 48; - break; - default: - - // Note: Twitter's big avatars are a different size than - // Laconica's (Laconica's = 96) - - $avatar->width = 73; - $avatar->height = 73; - } - - $avatar->original = 0; // we don't have the original - $avatar->mediatype = $mediatype; - $avatar->filename = $filename; - $avatar->url = Avatar::url($filename); - - common_debug("new filename: $avatar->url"); - if (defined('SCRIPT_DEBUG')) { - print "New filename: $avatar->url\n"; - } - - $avatar->created = common_sql_now(); - - $id = $avatar->insert(); - - if (!$id) { - common_log_db_error($avatar, 'INSERT', __FILE__); + common_debug("Saved new $size avatar for $profile_id."); if (defined('SCRIPT_DEBUG')) { - print "Could not insert avatar!\n"; + print "Saved new $size avatar for $profile_id.\n"; } - return null; + return $id; } - common_debug("Saved new $size avatar for $profile_id."); - if (defined('SCRIPT_DEBUG')) { - print "Saved new $size avatar for $profile_id.\n"; - } + function fetchAvatar($url, $filename) + { + $avatar_dir = INSTALLDIR . '/avatar/'; - return $id; -} + $avatarfile = $avatar_dir . $filename; -function fetchAvatar($url, $filename) -{ - $avatar_dir = INSTALLDIR . '/avatar/'; + $out = fopen($avatarfile, 'wb'); + if (!$out) { + common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__); + if (defined('SCRIPT_DEBUG')) { + print "Couldn't open file! $filename\n"; + } + return false; + } - $avatarfile = $avatar_dir . $filename; - - $out = fopen($avatarfile, 'wb'); - if (!$out) { - common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__); + common_debug("Fetching avatar: $url", __FILE__); if (defined('SCRIPT_DEBUG')) { - print "Couldn't open file! $filename\n"; + print "Fetching avatar from Twitter: $url\n"; } - return false; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_FILE, $out); + curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + $result = curl_exec($ch); + curl_close($ch); + + fclose($out); + + return $result; } - - common_debug("Fetching avatar: $url", __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "Fetching avatar from Twitter: $url\n"; - } - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_FILE, $out); - curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); - $result = curl_exec($ch); - curl_close($ch); - - fclose($out); - - return $result; } + +ini_set("max_execution_time", "0"); +ini_set("max_input_time", "0"); +set_time_limit(0); +mb_internal_encoding('UTF-8'); +declare(ticks = 1); + +$fetcher = new TwitterStatusFetcher(); +$fetcher->runOnce(); + From 2621a5471f9a3fa75d206ed5b3a4a91df1e28bdc Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 00:26:42 -0700 Subject: [PATCH 73/83] Better name --- scripts/{statusfetcher.php => twitterstatusfetcher.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{statusfetcher.php => twitterstatusfetcher.php} (100%) diff --git a/scripts/statusfetcher.php b/scripts/twitterstatusfetcher.php similarity index 100% rename from scripts/statusfetcher.php rename to scripts/twitterstatusfetcher.php From 856e05a08ff8d09fbd580ed35906e3dda0475a0a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 01:10:31 -0700 Subject: [PATCH 74/83] Less pychotic debugging statements --- scripts/twitterstatusfetcher.php | 140 ++++++++++++------------------- 1 file changed, 52 insertions(+), 88 deletions(-) mode change 100644 => 100755 scripts/twitterstatusfetcher.php diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php old mode 100644 new mode 100755 index 5275a45752..e8819f6651 --- a/scripts/twitterstatusfetcher.php +++ b/scripts/twitterstatusfetcher.php @@ -72,10 +72,8 @@ class TwitterStatusFetcher extends Daemon if ($pid) { // Parent - common_debug("Parent: forked new status fetcher process " . $pid); - if (defined('SCRIPT_DEBUG')) { - print "Parent: forked fetcher process " . $pid . "\n"; + common_debug("Parent: forked new status fetcher process " . $pid); } $this->children[] = $pid; @@ -90,10 +88,8 @@ class TwitterStatusFetcher extends Daemon // Remove child from ps list as it finishes while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { - common_debug("Child $c finished."); - if (defined('SCRIPT_DEBUG')) { - print "Child $c finished.\n"; + common_debug("Child $c finished."); } $this->remove_ps($this->children, $c); @@ -102,18 +98,14 @@ class TwitterStatusFetcher extends Daemon // Wait! We have too many damn kids. if (sizeof($this->children) > MAXCHILDREN) { - common_debug('Too many children. Waiting...'); - if (defined('SCRIPT_DEBUG')) { - print "Too many children. Waiting...\n"; + common_debug('Too many children. Waiting...'); } if (($c = pcntl_wait($status, WUNTRACED)) > 0){ - common_debug("Finished waiting for $c"); - if (defined('SCRIPT_DEBUG')) { - print "Finished waiting for $c\n"; + common_debug("Finished waiting for $c"); } $this->remove_ps($this->children, $c); @@ -124,21 +116,18 @@ class TwitterStatusFetcher extends Daemon // Remove all children from the process list before restarting while(($c = pcntl_wait($status, WUNTRACED)) > 0) { - common_debug("Child $c finished."); - if (defined('SCRIPT_DEBUG')) { - print "Child $c finished.\n"; + common_debug("Child $c finished."); } $this->remove_ps($this->children, $c); } // Rest for a bit before we fetch more statuses - common_debug('Waiting ' . POLL_INTERVAL . - ' secs before hitting Twitter again.'); + if (defined('SCRIPT_DEBUG')) { - print 'Waiting ' . POLL_INTERVAL . - " secs before hitting Twitter again.\n"; + common_debug('Waiting ' . POLL_INTERVAL . + ' secs before hitting Twitter again.'); } sleep(POLL_INTERVAL); @@ -155,14 +144,16 @@ class TwitterStatusFetcher extends Daemon $cnt = $flink->find(); if (defined('SCRIPT_DEBUG')) { - print "Updating Twitter friends subscriptions for $cnt users.\n"; + common_debug('Updating Twitter friends subscriptions' . + " for $cnt users."); } $flinks = array(); while ($flink->fetch()) { - if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) { + if (($flink->noticesync & FOREIGN_NOTICE_RECV) == + FOREIGN_NOTICE_RECV) { $flinks[] = clone($flink); } } @@ -187,30 +178,28 @@ class TwitterStatusFetcher extends Daemon { if (empty($flink)) { - common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid"); - if (defined('SCRIPT_DEBUG')) { - print "Can't retrieve Foreign_link for foreign ID $fid\n"; - } + common_log(LOG_WARNING, + "Can't retrieve Foreign_link for foreign ID $fid"); return; } $fuser = $flink->getForeignUser(); if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - if (defined('SCRIPT_DEBUG')) { - print "Unmatched user for ID $flink->user_id\n"; - } + common_log(LOG_WARNING, "Unmatched user for ID " . + $flink->user_id); return; } - common_debug('Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id)."); if (defined('SCRIPT_DEBUG')) { - print 'Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id).\n"; + common_debug('Trying to get timeline for Twitter user ' . + "$fuser->nickname ($flink->foreign_id)."); } + // XXX: Biggest remaining issue - How do we know at which status + // to start importing? How many statuses? Right now I'm going + // with the default last 20. + $url = 'http://twitter.com/statuses/friends_timeline.json'; $timeline_json = get_twitter_data($url, $fuser->nickname, @@ -220,18 +209,19 @@ class TwitterStatusFetcher extends Daemon if (empty($timeline)) { common_log(LOG_WARNING, "Empty timeline."); - if (defined('SCRIPT_DEBUG')) { - print "Empty timeline!\n"; - } return; } foreach ($timeline as $status) { - // Hacktastic: filter out stuff coming from Laconica + // Hacktastic: filter out stuff coming from this Laconica $source = mb_strtolower(common_config('integration', 'source')); if (preg_match("/$source/", mb_strtolower($status->source))) { + if (defined('SCRIPT_DEBUG')) { + common_debug('Skipping import of status ' . $status->id . + ' with source ' . $source); + } continue; } @@ -239,7 +229,6 @@ class TwitterStatusFetcher extends Daemon } // Okay, record the time we synced with Twitter for posterity - $flink->last_noticesync = common_sql_now(); $flink->update(); } @@ -250,18 +239,14 @@ class TwitterStatusFetcher extends Daemon $profile = Profile::staticGet($id); if (!$profile) { - common_log(LOG_ERR, 'Problem saving notice. No associated Profile.'); - if (defined('SCRIPT_DEBUG')) { - print "Problem saving notice. No associated Profile.\n"; - } + common_log(LOG_ERR, + 'Problem saving notice. No associated Profile.'); return null; } $uri = 'http://twitter.com/' . $status->user->screen_name . '/status/' . $status->id; - // Skip save if notice source is Laconica or Identi.ca? - $notice = Notice::staticGet('uri', $uri); // check to see if we've already imported the status @@ -290,21 +275,23 @@ class TwitterStatusFetcher extends Daemon if (!$notice_id) { common_log_db_error($notice, 'INSERT', __FILE__); if (defined('SCRIPT_DEBUG')) { - print "Could not save notice!\n"; + common_debug('Could not save notice!'); } } - // XXX: Figure out a better way to link replies? + // XXX: Figure out a better way to link Twitter replies? $notice->saveReplies(); - // XXX: Do we want to polute our tag cloud with hashtags from Twitter? + // XXX: Do we want to polute our tag cloud with + // hashtags from Twitter? $notice->saveTags(); $notice->saveGroups(); $notice->query('COMMIT'); if (defined('SCRIPT_DEBUG')) { - print "Saved status $status->id as notice $notice->id.\n"; + common_debug("Saved status $status->id" . + " as notice $notice->id."); } } @@ -327,18 +314,19 @@ class TwitterStatusFetcher extends Daemon $profile = Profile::staticGet('profileurl', $profileurl); if ($profile) { - common_debug("Profile for $profile->nickname found."); + if (defined('SCRIPT_DEBUG')) { + common_debug("Profile for $profile->nickname found."); + } // Check to see if the user's Avatar has changed $this->checkAvatar($user, $profile); + return $profile->id; } else { - $debugmsg = 'Adding profile and remote profile ' . - "for Twitter user: $profileurl\n"; - common_debug($debugmsg, __FILE__); if (defined('SCRIPT_DEBUG')) { - print $debugmsg; + common_debug('Adding profile and remote profile ' . + "for Twitter user: $profileurl"); } $profile = new Profile(); @@ -356,10 +344,6 @@ class TwitterStatusFetcher extends Daemon if (empty($id)) { common_log_db_error($profile, 'INSERT', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print 'Could not insert Profile: ' . - common_log_objstring($profile) . "\n"; - } $profile->query("ROLLBACK"); return false; } @@ -379,10 +363,6 @@ class TwitterStatusFetcher extends Daemon if (empty($rid)) { common_log_db_error($profile, 'INSERT', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print 'Could not insert Remote_profile: ' . - common_log_objstring($remote_pro) . "\n"; - } $profile->query("ROLLBACK"); return false; } @@ -408,13 +388,10 @@ class TwitterStatusFetcher extends Daemon if ($newname != $oldname) { - common_debug("Avatar for Twitter user $profile->nickname has changed."); - common_debug("old: $oldname new: $newname"); - if (defined('SCRIPT_DEBUG')) { - print "Avatar for Twitter user $user->id has changed.\n"; - print "old: $oldname\n"; - print "new: $newname\n"; + common_debug('Avatar for Twitter user ' . + "$profile->nickname has changed."); + common_debug("old: $oldname new: $newname"); } $img_root = substr($path_parts['basename'], 0, -11); @@ -472,26 +449,21 @@ class TwitterStatusFetcher extends Daemon $this->newAvatar($id, $size, $mediatype, $filename); } else { common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "Problem fetching Avatar: $url\n"; - } } } } function updateAvatar($profile_id, $size, $mediatype, $filename) { - common_debug("Updating avatar: $size"); if (defined('SCRIPT_DEBUG')) { - print "Updating avatar: $size\n"; + common_debug("Updating avatar: $size"); } $profile = Profile::staticGet($profile_id); if (!$profile) { - common_debug("Couldn't get profile: $profile_id!"); if (defined('SCRIPT_DEBUG')) { - print "Couldn't get profile: $profile_id!\n"; + common_debug("Couldn't get profile: $profile_id!"); } return; } @@ -500,7 +472,9 @@ class TwitterStatusFetcher extends Daemon $avatar = $profile->getAvatar($sizes[$size]); if ($avatar) { - common_debug("Deleting $size avatar for $profile->nickname."); + if (defined('SCRIPT_DEBUG')) { + common_debug("Deleting $size avatar for $profile->nickname."); + } @unlink(INSTALLDIR . '/avatar/' . $avatar->filename); $avatar->delete(); } @@ -538,9 +512,8 @@ class TwitterStatusFetcher extends Daemon $avatar->filename = $filename; $avatar->url = Avatar::url($filename); - common_debug("new filename: $avatar->url"); if (defined('SCRIPT_DEBUG')) { - print "New filename: $avatar->url\n"; + common_debug("new filename: $avatar->url"); } $avatar->created = common_sql_now(); @@ -549,16 +522,11 @@ class TwitterStatusFetcher extends Daemon if (!$id) { common_log_db_error($avatar, 'INSERT', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "Could not insert avatar!\n"; - } - return null; } - common_debug("Saved new $size avatar for $profile_id."); if (defined('SCRIPT_DEBUG')) { - print "Saved new $size avatar for $profile_id.\n"; + common_debug("Saved new $size avatar for $profile_id."); } return $id; @@ -573,15 +541,11 @@ class TwitterStatusFetcher extends Daemon $out = fopen($avatarfile, 'wb'); if (!$out) { common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't open file! $filename\n"; - } return false; } - common_debug("Fetching avatar: $url", __FILE__); if (defined('SCRIPT_DEBUG')) { - print "Fetching avatar from Twitter: $url\n"; + common_debug("Fetching avatar: $url"); } $ch = curl_init(); From bc190595d1dfd56bf7e68597b3d574909eb27260 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 02:07:31 -0700 Subject: [PATCH 75/83] Added TwitterStatusFetcher into daemon startup and shutdown subsystem --- config.php.sample | 3 +++ lib/common.php | 2 ++ scripts/getvaliddaemons.php | 3 +++ scripts/stopdaemons.sh | 2 +- scripts/twitterstatusfetcher.php | 10 ++++++---- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/config.php.sample b/config.php.sample index b8ed45fa83..6d6a9b533a 100644 --- a/config.php.sample +++ b/config.php.sample @@ -150,6 +150,9 @@ $config['sphinx']['port'] = 3312; #$config['memcached']['server'] = 'localhost'; #$config['memcached']['port'] = 11211; +# Enable bidirectional Twitter bridge +#$config['twitterbridge']['enabled'] = true; + #Twitter integration source attribute. Note: default is Laconica #$config['integration']['source'] = 'Laconica'; diff --git a/lib/common.php b/lib/common.php index 00e5b0bc29..abdc22c0e3 100644 --- a/lib/common.php +++ b/lib/common.php @@ -143,6 +143,8 @@ $config = array('piddir' => '/var/run', 'user' => false, 'group' => false), + 'twitterbridge' => + array('enabled' => false), 'integration' => array('source' => 'Laconica', # source attribute for Twitter 'taguri' => $_server.',2009'), # base for tag URIs diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php index 482e63af70..a10233e69f 100755 --- a/scripts/getvaliddaemons.php +++ b/scripts/getvaliddaemons.php @@ -44,6 +44,9 @@ if(common_config('xmpp','enabled')) { if(common_config('memcached','enabled')) { echo "memcachedqueuehandler.php "; } +if(common_config('twitterbridge','enabled')) { + echo "twitterstatusfetcher.php "; +} echo "ombqueuehandler.php "; echo "twitterqueuehandler.php "; echo "facebookqueuehandler.php "; diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index f6d71eddfb..764037e8ff 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ - memcachehandler inboxhandler; do + memcachehandler inboxhandler twitterstatusfetcher; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php index e8819f6651..9dfadc7606 100755 --- a/scripts/twitterstatusfetcher.php +++ b/scripts/twitterstatusfetcher.php @@ -32,7 +32,7 @@ define('LACONICA', true); define('MAXCHILDREN', 2); define('POLL_INTERVAL', 60); // in seconds -// Uncomment this to get useful console output +// Uncomment this to get useful logging define('SCRIPT_DEBUG', true); require_once(INSTALLDIR . '/lib/common.php'); @@ -45,7 +45,7 @@ class TwitterStatusFetcher extends Daemon function name() { - return 'twitterstatusfetcher'; + return ('twitterstatusfetcher.generic'); } function run() @@ -130,7 +130,9 @@ class TwitterStatusFetcher extends Daemon ' secs before hitting Twitter again.'); } - sleep(POLL_INTERVAL); + if (POLL_INTERVAL > 0) { + sleep(POLL_INTERVAL); + } } while (true); } @@ -282,7 +284,7 @@ class TwitterStatusFetcher extends Daemon // XXX: Figure out a better way to link Twitter replies? $notice->saveReplies(); - // XXX: Do we want to polute our tag cloud with + // XXX: Do we want to pollute our tag cloud with // hashtags from Twitter? $notice->saveTags(); $notice->saveGroups(); From 5771f413bb28502540d3bc017bc58433e9b0abf9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 02:08:49 -0700 Subject: [PATCH 76/83] Fil's Patch to DB_DataObject to make it reconnect to the DB if there's no connection. This patch has been added upstream and will be in the next release, but I need it now for the bidirectional bridge to work. --- extlib/DB/DataObject.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php index b1a1a4e218..0c6a13dc28 100644 --- a/extlib/DB/DataObject.php +++ b/extlib/DB/DataObject.php @@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload $t= explode(' ',microtime()); $_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1]; + + do { if ($_DB_driver == 'DB') { $result = $DB->query($string); @@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload break; } } - - + + // try to reconnect, at most 3 times + $again = false; + if (is_a($result, 'PEAR_Error') + AND $result->getCode() == DB_ERROR_NODBSELECTED + AND $cpt++<3) { + $DB->disconnect(); + sleep(1); + $DB->connect($DB->dsn); + $again = true; + } + + } while ($again); if (is_a($result,'PEAR_Error')) { if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { From 4b0e5ff271d4ac0af3256b2716f1e1362ddb02d8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 14:07:03 -0700 Subject: [PATCH 77/83] Added Twitter to notice sources --- db/notice_source.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/db/notice_source.sql b/db/notice_source.sql index ce44f32354..e6e2180a57 100644 --- a/db/notice_source.sql +++ b/db/notice_source.sql @@ -43,6 +43,7 @@ VALUES ('twidge','Twidge','http://software.complete.org/twidge', now()), ('twidroid','twidroid','http://www.twidroid.com/', now()), ('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()), + ('twitter','Twitter','http://twitter.com/', now()), ('twitterfeed','twitterfeed','http://twitterfeed.com/', now()), ('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()), ('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()), From fbf23ae0ee4c8c63e80e3511aa7fce980b8d1ed5 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 14:41:53 -0700 Subject: [PATCH 78/83] Only show import friends timeline option if bidirectional bridge enabled --- actions/twittersettings.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/actions/twittersettings.php b/actions/twittersettings.php index 580d9ecf76..1bce576951 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -158,13 +158,22 @@ class TwittersettingsAction extends ConnectSettingsAction ($flink->friendsync & FOREIGN_FRIEND_RECV) : false); $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('noticerecv', - _('Import my Friends Timeline.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_RECV) : - false); - $this->elementEnd('li'); + + if (common_config('twitterbridge','enabled')) { + $this->elementStart('li'); + $this->checkbox('noticerecv', + _('Import my Friends Timeline.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_RECV) : + false); + $this->elementEnd('li'); + } else { + // preserve setting even if bidrection bridge toggled off + if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { + $this->hidden('noticerecv', true, 'noticerecv'); + } + } + $this->elementEnd('ul'); if ($flink) { From 3e7b1e69e3e97ac007465376b62084f10bcf97ca Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 28 Apr 2009 17:08:20 -0700 Subject: [PATCH 79/83] Added dirty dates to Foreign_link --- classes/Foreign_link.php | 2 ++ classes/laconica.ini | 2 ++ db/laconica.sql | 2 ++ 3 files changed, 6 insertions(+) diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php index afc0e21804..af2b3f189d 100644 --- a/classes/Foreign_link.php +++ b/classes/Foreign_link.php @@ -17,6 +17,8 @@ class Foreign_link extends Memcached_DataObject public $noticesync; // tinyint(1) not_null default_1 public $friendsync; // tinyint(1) not_null default_2 public $profilesync; // tinyint(1) not_null default_1 + public $last_noticesync; // datetime() + public $last_friendsync; // datetime() public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP diff --git a/classes/laconica.ini b/classes/laconica.ini index 529454d99b..c054195884 100755 --- a/classes/laconica.ini +++ b/classes/laconica.ini @@ -55,6 +55,8 @@ credentials = 2 noticesync = 145 friendsync = 145 profilesync = 145 +last_noticesync = 14 +last_friendsync = 14 created = 142 modified = 384 diff --git a/db/laconica.sql b/db/laconica.sql index 5b57494d98..c9730098e3 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -289,6 +289,8 @@ create table foreign_link ( noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies', friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', + last_noticesync datetime default null comment 'last time notices were imported', + last_friendsync datetime default null comment 'last time friends were imported', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', From 11e0db8c2cec18337fd960ccda055dd14d89f9d7 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 May 2009 18:22:14 -0700 Subject: [PATCH 80/83] Twitter friends sync now does 25 users at a time and uses last_friendsync field to prioritize --- actions/twittersettings.php | 4 +++- scripts/synctwitterfriends.php | 38 ++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/actions/twittersettings.php b/actions/twittersettings.php index 45725d3ff4..0b98eef591 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -261,7 +261,7 @@ class TwittersettingsAction extends ConnectSettingsAction 'alt' => ($other->fullname) ? $other->fullname : $other->nickname)); - + $this->element('span', 'fn nickname', $other->nickname); $this->elementEnd('a'); $this->elementEnd('li'); @@ -375,6 +375,8 @@ class TwittersettingsAction extends ConnectSettingsAction if ($friendsync) { save_twitter_friends($user, $twit_user->id, $screen_name, $password); + $flink->last_friendsync = common_sql_now(); + $flink->update(); } $this->showForm(_('Twitter settings saved.'), true); diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php index 794301f0f0..bd08ba58d6 100755 --- a/scripts/synctwitterfriends.php +++ b/scripts/synctwitterfriends.php @@ -32,8 +32,25 @@ define('LACONICA', true); require_once(INSTALLDIR . '/lib/common.php'); +// Make a lockfile +$lockfilename = lockFilename(); +if (!($lockfile = @fopen($lockfilename, "w"))) { + print "Already running... exiting.\n"; + exit(1); +} + +// Obtain an exlcusive lock on file (will fail if script is already going) +if (!@flock( $lockfile, LOCK_EX | LOCK_NB, &$wouldblock) || $wouldblock) { + // Script already running - abort + @fclose($lockfile); + print "Already running... exiting.\n"; + exit(1); +} + $flink = new Foreign_link(); $flink->service = 1; // Twitter +$flink->orderBy('last_friendsync'); +$flink->limit(25); // sync this many users during this run $cnt = $flink->find(); print "Updating Twitter friends subscriptions for $cnt users.\n"; @@ -60,8 +77,11 @@ while ($flink->fetch()) { continue; } - $result = save_twitter_friends($user, $fuser->id, - $fuser->nickname, $flink->credentials); + save_twitter_friends($user, $fuser->id, $fuser->nickname, $flink->credentials); + + $flink->last_friendsync = common_sql_now(); + $flink->update(); + if (defined('SCRIPT_DEBUG')) { print "\nDONE\n"; } else { @@ -70,4 +90,18 @@ while ($flink->fetch()) { } } +function lockFilename() +{ + $piddir = common_config('daemon', 'piddir'); + if (!$piddir) { + $piddir = '/var/run'; + } + + return $piddir . '/synctwitterfriends.lock'; +} + +// Cleanup +fclose($lockfile); +unlink($lockfilename); + exit(0); From 9a8095079dc602c7f2b74e48237445d682844de6 Mon Sep 17 00:00:00 2001 From: CiaranG Date: Fri, 8 May 2009 08:14:50 +0100 Subject: [PATCH 81/83] PostgreSQL - added dirty dates to Foreign_link - see 3e7b1e69e3e97ac007465376b62084f10bcf97ca --- db/laconica_pg.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql index f879d7936f..a27a616f24 100644 --- a/db/laconica_pg.sql +++ b/db/laconica_pg.sql @@ -291,6 +291,8 @@ create table foreign_link ( noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */, friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */, profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */, + last_noticesync timestamp default null /* comment 'last time notices were imported' */, + last_friendsync timestamp default null /* comment 'last time friends were imported' */, created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */, modified timestamp /* comment 'date this record was modified' */, From 8fc8eaa1b6199d74f0e17197650c6316b4594203 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 10 May 2009 23:14:42 +0000 Subject: [PATCH 82/83] Init biz theme Design is similar (inspired) to http://drupal.org/project/acquia_marina Two of the background illustrations are reused. --- theme/biz/css/base.css | 1170 +++++++++++++++++ theme/biz/css/display.css | 252 ++++ theme/biz/css/ie.css | 9 + theme/biz/default-avatar-mini.png | Bin 0 -> 646 bytes theme/biz/default-avatar-profile.png | Bin 0 -> 2853 bytes theme/biz/default-avatar-stream.png | Bin 0 -> 1487 bytes .../images/illustrations/illu_pattern-01.png | Bin 0 -> 935 bytes .../images/illustrations/illu_pattern-02.png | Bin 0 -> 9498 bytes theme/biz/logo.png | Bin 0 -> 4988 bytes 9 files changed, 1431 insertions(+) create mode 100644 theme/biz/css/base.css create mode 100644 theme/biz/css/display.css create mode 100644 theme/biz/css/ie.css create mode 100644 theme/biz/default-avatar-mini.png create mode 100644 theme/biz/default-avatar-profile.png create mode 100644 theme/biz/default-avatar-stream.png create mode 100644 theme/biz/images/illustrations/illu_pattern-01.png create mode 100644 theme/biz/images/illustrations/illu_pattern-02.png create mode 100644 theme/biz/logo.png diff --git a/theme/biz/css/base.css b/theme/biz/css/base.css new file mode 100644 index 0000000000..22bbced08c --- /dev/null +++ b/theme/biz/css/base.css @@ -0,0 +1,1170 @@ +/** theme: biz base + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +* { margin:0; padding:0; } +img { display:block; border:0; } +a abbr { cursor: pointer; border-bottom:0; } +table { border-collapse:collapse; } +ol { list-style-position:inside; } +html { font-size: 87.5%; background-color:#fff; height:100%; } +body { +background-color:#fff; +color:#000; +font-family:sans-serif; +font-size:1em; +line-height:1.65; +position:relative; +} +h1,h2,h3,h4,h5,h6 { +margin-bottom:7px; +overflow:hidden; +} +h1 { +font-size:1.4em; +margin-bottom:18px; +} +#showstream h1 { display:none; } +h2 { font-size:1.3em; } +h3 { font-size:1.2em; } +h4 { font-size:1.1em; } +h5 { font-size:1em; } +h6 { font-size:0.9em; } + +caption { +font-weight:bold; +} +legend { +font-weight:bold; +font-size:1.3em; +} +input, textarea, select, option { +padding:4px; +font-family:sans-serif; +font-size:1em; +} +input, textarea, select { +border-width:2px; +border-style: solid; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} + +input.submit { +font-weight:bold; +cursor:pointer; +} +textarea { +overflow:auto; +} +option { +padding-bottom:0; +} +fieldset { +padding:0; +border:0; +} +form ul li { +list-style-type:none; +margin:0 0 18px 0; +} +form label { +font-weight:bold; +} +input.checkbox { +position:relative; +top:2px; +left:0; +border:0; +} + +.error, +.success { +padding:4px 1.55%; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +margin-bottom:18px; +} +form label.submit { +display:none; +} + +.form_settings { +clear:both; +} + +.form_settings fieldset { +margin-bottom:29px; +} +.form_settings input.remove { +margin-left:11px; +} +.form_settings .form_data li { +width:100%; +float:left; +} +.form_settings .form_data label { +float:left; +} +.form_settings .form_data textarea, +.form_settings .form_data select, +.form_settings .form_data input { +margin-left:11px; +float:left; +} +.form_settings .form_data input.submit { +margin-left:0; +} + +.form_settings label { +margin-top:2px; +width:113px; +} + +.form_actions label { +display:none; +} +.form_guide { +font-style:italic; +} + +.form_settings #settings_autosubscribe label { +display:inline; +font-weight:bold; +} + +#form_settings_profile legend, +#form_login legend, +#form_register legend, +#form_password legend, +#form_settings_avatar legend, +#newgroup legend, +#editgroup legend, +#form_tag_user legend, +#form_remote_subscribe legend, +#form_openid_login legend, +#form_search legend, +#form_invite legend, +#form_notice_delete legend, +#form_password_recover legend, +#form_password_change legend { +display:none; +} + +.form_settings .form_data p.form_guide { +clear:both; +margin-left:124px; +margin-bottom:0; +} + +.form_settings p { +margin-bottom:11px; +} + +.form_settings input.checkbox { +margin-top:3px; +margin-left:0; +} +.form_settings label.checkbox { +font-weight:normal; +margin-top:0; +margin-right:0; +margin-left:11px; +float:left; +width:90%; +} + + +#form_login p.form_guide, +#form_register #settings_rememberme p.form_guide, +#form_openid_login #settings_rememberme p.form_guide, +#settings_twitter_remove p.form_guide, +#form_search ul.form_data #q { +margin-left:0; +} + +.form_settings .form_note { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +padding:0 7px; +} + + +.form_settings input.form_action-secondary { +margin-left:29px; +padding:0; +} + +#form_search .submit { +margin-left:11px; +} + +address { +float:left; +margin-bottom:18px; +margin-left:18px; +} +address.vcard img.logo { +margin-right:0; +} +address .fn { +font-weight:bold; +} +address img + .fn { +display:none; +} + +#header { +width:100%; +position:relative; +float:left; +padding-top:18px; +margin-bottom:18px; +} + +#site_nav_global_primary { +float:left; +margin-right:18px; +margin-bottom:11px; +width:50%; +} +#site_nav_global_primary ul li { +display:inline; +margin-right:11px; +} + +.system_notice dt { +font-weight:bold; +text-transform:uppercase; +display:none; +} + +#site_notice { +float:right; +clear:right; +margin-top:7px; +margin-right:18px; +width:24%; +} +#page_notice { +clear:both; +margin-bottom:18px; +} + + +#anon_notice { +float:left; +width:45.4%; +/* +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +border-width:2px; +border-style:solid; +*/ +line-height:1.5; +font-size:1.1em; +font-weight:bold; +} + + +#footer { +float:left; +width:64%; +padding:18px; +} + +#site_nav_local_views { +width:14.5%; +float:left; +} +#site_nav_local_views dt { +display:none; +} +#site_nav_local_views li { +list-style-type:none; +} +#site_nav_local_views a { +display:block; +text-decoration:none; +padding:4px 11px; +-moz-border-radius-topleft:4px; +-moz-border-radius-bottomleft:4px; +-webkit-border-top-left-radius:4px; +-webkit-border-bottom-left-radius:4px; +border-width:1px; +border-style:solid; +border-right:0; +text-shadow: 2px 2px 2px #ddd; +font-weight:bold; +} +#site_nav_local_views .nav { +float:left; +width:100%; +} + +#site_nav_global_primary dt, +#site_nav_global_secondary dt { +display:none; +} + +#site_nav_global_secondary { +margin-bottom:11px; +} + +#site_nav_global_secondary ul li { +display:inline; +margin-right:11px; +} +#export_data li a { +padding-left:20px; +} +#export_data li a.foaf { +padding-left:30px; +} +#export_data li a.export_vcard { +padding-left:28px; +} + +#export_data ul { +display:inline; +} +#export_data li { +list-style-type:none; +display:inline; +margin-left:11px; +} +#export_data li:first-child { +margin-left:0; +} + +#licenses { +font-size:0.9em; +} + +#licenses dt { +font-weight:bold; +display:none; +} +#licenses dd { +margin-bottom:11px; +line-height:1.5; +} + +#site_content_license_cc { +margin-bottom:0; +} +#site_content_license_cc img { +display:inline; +vertical-align:top; +margin-right:4px; +} + +#wrap { +margin:0 auto; +width:100%; +min-width:760px; +max-width:1003px; +overflow:hidden; +} + +#core { +position:relative; +width:100%; +float:left; +margin-bottom:1em; +} + +#content { +width:51.009%; +min-height:259px; +padding:1.795%; +float:left; +border-radius:7px; +-moz-border-radius:7px; +-moz-border-radius-topleft:0; +-webkit-border-radius:7px; +-webkit-border-top-left-radius:0; +border-style:solid; +border-width:1px; +} +#shownotice #content { +min-height:0; +} + +#content_inner { +position:relative; +width:100%; +float:left; +} + +#aside_primary { +width:29.917%; +min-height:259px; +float:left; +margin-left:0.385%; +} + +#form_notice { +width:45.664%; +float:left; +position:relative; +line-height:1; +} +#form_notice fieldset { +border:0; +padding:0; +position:relative; +} +#form_notice legend { +display:none; +} +#form_notice textarea { +float:left; +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +width:80.789%; +height:67px; +line-height:1.5; +padding:7px 7px 16px 7px; +} +#form_notice label { +display:block; +float:left; +font-size:1.3em; +margin-bottom:7px; +} +#form_notice #notice_submit label { +display:none; +} +#form_notice .form_note { +position:absolute; +top:99px; +right:98px; +z-index:9; +} +#form_notice .form_note dt { +font-weight:bold; +display:none; +} +#notice_text-count { +font-weight:bold; +line-height:1.15; +padding:1px 2px; +} +#form_notice #notice_action-submit { +width:14%; +height:47px; +padding:0; +position:absolute; +bottom:0; +right:0; +} +#form_notice label[for=to] { +margin-top:7px; +} +#form_notice select[id=to] { +margin-bottom:7px; +margin-left:18px; +float:left; +} +#form_notice .error { +float:left; +clear:both; +width:96.9%; +margin-bottom:0; +line-height:1.618; +} + +/* entity_profile */ +.entity_profile { +position:relative; +width:67.702%; +min-height:123px; +float:left; +margin-bottom:18px; +margin-left:0; +overflow:hidden; +} +.entity_profile dt, +#entity_statistics dt { +font-weight:bold; +} +.entity_profile dd { +display:inline; +} + +.entity_profile .entity_depiction { +float:left; +width:96px; +margin-right:18px; +margin-bottom:18px; +} + +.entity_profile .entity_fn, +.entity_profile .entity_nickname, +.entity_profile .entity_location, +.entity_profile .entity_url, +.entity_profile .entity_note, +.entity_profile .entity_tags { +margin-left:113px; +margin-bottom:4px; +} + +.entity_profile .entity_fn, +.entity_profile .entity_nickname { +margin-left:11px; +display:inline; +font-weight:bold; +} +.entity_profile .entity_nickname { +margin-left:0; +} + +.entity_profile .entity_fn dd:before { +content: "("; +font-weight:normal; +} +.entity_profile .entity_fn dd:after { +content: ")"; +font-weight:normal; +} + +.entity_profile dt { +display:none; +} +.entity_profile h2 { +display:none; +} +/* entity_profile */ + + +/*entity_actions*/ +.entity_actions { +float:right; +margin-left:4.35%; +max-width:25%; +} +.entity_actions h2 { +display:none; +} +.entity_actions ul { +list-style-type:none; +} +.entity_actions li { +margin-bottom:4px; +} +.entity_actions li:first-child { +border-top:0; +} +.entity_actions fieldset { +border:0; +padding:0; +} +.entity_actions legend { +display:none; +} + +.entity_actions input.submit { +display:block; +text-align:left; +width:100%; +} +.entity_actions a, +.entity_nudge p, +.entity_remote_subscribe { +text-decoration:none; +font-weight:bold; +display:block; +} + +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_send-a-message a, +.entity_edit a, +.form_user_nudge input.submit, +.entity_nudge p { +border:0; +padding-left:20px; +} + +.entity_edit a, +.entity_send-a-message a, +.entity_nudge p { +padding:4px 4px 4px 23px; +} + +.entity_remote_subscribe { +padding:4px; +border-width:2px; +border-style:solid; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} +.entity_actions .accept { +margin-bottom:18px; +} + +.entity_tags ul { +list-style-type:none; +display:inline; +} +.entity_tags li { +display:inline; +margin-right:4px; +} + +.aside .section { +margin-bottom:18px; +clear:both; +float:left; +width:87.985%; +padding:6%; +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +border-width:1px; +border-style:solid; +} +.aside .section h2 { +text-transform:uppercase; +font-size:1em; +} + +#entity_statistics dt, +#entity_statistics dd { +display:inline; +} +#entity_statistics dt:after { +content: ":"; +} + +.section ul.entities { +float:left; +width:100%; +} +.section .entities li { +list-style-type:none; +float:left; +margin-right:7px; +margin-bottom:7px; +} +.section .entities li .photo { +margin-right:0; +margin-bottom:0; +} +.section .entities li .fn { +display:none; +} + +.aside .section p, +.aside .section .more { +clear:both; +} + +.profile .entity_profile { +margin-bottom:0; +min-height:60px; +} + + +.profile .form_group_join legend, +.profile .form_group_leave legend, +.profile .form_user_subscribe legend, +.profile .form_user_unsubscribe legend { +display:none; +} + +.profiles { +list-style-type:none; +} +.profile .entity_profile .entity_location { +width:auto; +clear:none; +margin-left:11px; +} +.profile .entity_profile dl, +.profile .entity_profile dd { +display:inline; +float:none; +} +.profile .entity_profile .entity_note, +.profile .entity_profile .entity_url, +.profile .entity_profile .entity_tags, +.profile .entity_profile .form_subscription_edit { +margin-left:59px; +clear:none; +display:block; +width:auto; +} +.profile .entity_profile .entity_tags dt { +display:inline; +margin-right:11px; +} + + +.profile .entity_profile .form_subscription_edit label { +font-weight:normal; +margin-right:11px; +} + + +/* NOTICE */ +.notice, +.profile { +position:relative; +padding-top:11px; +padding-bottom:11px; +clear:both; +float:left; +width:100%; +border-top-width:1px; +border-top-style:dotted; +} +.notices li { +list-style-type:none; +} +.notices li.hover { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} + +/* NOTICES */ +#notices_primary { +float:left; +width:100%; +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +} +#notices_primary h2 { +display:none; +} +.notice-data a span { +display:block; +padding-left:28px; +} + +.notice .author { +margin-right:11px; +} + +.fn { +overflow:hidden; +} + +.notice .author .fn { +font-weight:bold; +} + +.vcard .photo { +display:inline; +margin-right:11px; +float:left; +} +#shownotice .vcard .photo { +margin-bottom:4px; +} +.vcard .url { +text-decoration:none; +} +.vcard .url:hover { +text-decoration:underline; +} + +.notice .entry-title { +float:left; +width:100%; +overflow:hidden; +} +#shownotice .notice .entry-title { +font-size:2.2em; +} + +.notice p.entry-content { +display:inline; +} + +#content .notice p.entry-content a:visited { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} +.notice p.entry-content .vcard a { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} + +.notice div.entry-content { +clear:left; +float:left; +font-size:0.95em; +margin-left:59px; +width:65%; +} +#showstream .notice div.entry-content, +#shownotice .notice div.entry-content { +margin-left:0; +} + +.notice .notice-options a, +.notice .notice-options input { +float:left; +font-size:1.025em; +} + +.notice div.entry-content dl, +.notice div.entry-content dt, +.notice div.entry-content dd { +display:inline; +} + +.notice div.entry-content .timestamp dt, +.notice div.entry-content .response dt { +display:none; +} +.notice div.entry-content .timestamp a { +display:inline-block; +} +.notice div.entry-content .device dt { +text-transform:lowercase; +} + + +.notice-options { +padding-left:2%; +float:left; +width:50%; +position:relative; +font-size:0.95em; +width:12.5%; +float:right; +} + +.notice-options a { +float:left; +} +.notice-options .notice_delete, +.notice-options .notice_reply, +.notice-options .form_favor, +.notice-options .form_disfavor { +position:absolute; +top:0; +} +.notice-options .form_favor, +.notice-options .form_disfavor { +left:0; +} +.notice-options .notice_reply { +left:29px; +} +.notice-options .notice_delete { +right:0; +} +.notice-options .notice_reply dt { +display:none; +} + +.notice-options input, +.notice-options a { +text-indent:-9999px; +outline:none; +} + +.notice-options .notice_reply a, +.notice-options input.submit { +display:block; +border:0; +} +.notice-options .notice_reply a, +.notice-options .notice_delete a { +text-decoration:none; +padding-left:16px; +} + +.notice-options form input.submit { +width:16px; +padding:2px 0; +} + +.notice-options .notice_delete dt, +.notice-options .form_favor legend, +.notice-options .form_disfavor legend { +display:none; +} +.notice-options .notice_delete fieldset, +.notice-options .form_favor fieldset, +.notice-options .form_disfavor fieldset { +border:0; +padding:0; +} + + +#usergroups #new_group { +float: left; +margin-right: 2em; +} +#new_group, #group_search { +margin-bottom:18px; +} +#new_group a { +padding-left:20px; +} + + +#filter_tags { +margin-bottom:11px; +float:left; +} +#filter_tags dt { +display:none; +} +#filter_tags ul { +list-style-type:none; +} +#filter_tags ul li { +float:left; +margin-left:7px; +padding-left:7px; +border-left-width:1px; +border-left-style:solid; +} +#filter_tags ul li.child_1 { +margin-left:0; +border-left:0; +padding-left:0; +} +#filter_tags ul li#filter_tags_all a { +font-weight:bold; +margin-top:7px; +float:left; +} + +#filter_tags ul li#filter_tags_item label { +margin-right:7px; +} +#filter_tags ul li#filter_tags_item label, +#filter_tags ul li#filter_tags_item select { +display:inline; +} +#filter_tags ul li#filter_tags_item p { +float:left; +margin-left:38px; +} +#filter_tags ul li#filter_tags_item input { +position:relative; +top:3px; +left:3px; +} + + + +.pagination { +float:left; +clear:both; +width:100%; +margin-top:18px; +} + +.pagination dt { +font-weight:bold; +display:none; +} + +.pagination .nav { +float:left; +width:100%; +list-style-type:none; +} + +.pagination .nav_prev { +float:left; +} +.pagination .nav_next { +float:right; +} + +.pagination a { +display:block; +text-decoration:none; +font-weight:bold; +padding:7px; +border-width:1px; +border-style:solid; +-moz-border-radius:7px; +-webkit-border-radius:7px; +border-radius:7px; +} + +.pagination .nav_prev a { +padding-left:30px; +} +.pagination .nav_next a { +padding-right:30px; +} +/* END: NOTICE */ + + +.hentry .entry-content p { +margin-bottom:18px; +} +.system_notice ul, +.instructions ul, +.hentry entry-content ol, +.hentry .entry-content ul { +list-style-position:inside; +} +.hentry .entry-content li { +margin-bottom:18px; +} +.hentry .entry-content li li { +margin-left:18px; +} + + + + +/* TOP_POSTERS */ +.section tbody td { +padding-right:11px; +padding-bottom:11px; +} +.section .vcard .photo { +margin-right:7px; +margin-bottom:0; +} + +.section .notice { +padding-top:7px; +padding-bottom:7px; +border-top:0; +} + +.section .notice:first-child { +padding-top:0; +} + +.section .notice .author { +margin-right:0; +} +.section .notice .author .fn { +display:none; +} + + +/* tagcloud */ +.tag-cloud { +list-style-type:none; +text-align:center; +} +.aside .tag-cloud { +font-size:0.8em; +} +.tag-cloud li { +display:inline; +margin-right:7px; +line-height:1.25; +} +.aside .tag-cloud li { +line-height:1.5; +} +.tag-cloud li a { +text-decoration:none; +} +#tagcloud.section dt { +text-transform:uppercase; +font-weight:bold; +} +.tag-cloud-1 { +font-size:1em; +} +.tag-cloud-2 { +font-size:1.25em; +} +.tag-cloud-3 { +font-size:1.75em; +} +.tag-cloud-4 { +font-size:2em; +} +.tag-cloud-5 { +font-size:2.25em; +} +.tag-cloud-6 { +font-size:2.75em; +} +.tag-cloud-7 { +font-size:3.25em; +} + +#publictagcloud #tagcloud.section dt { +display:none; +} + +#form_settings_photo .form_data { +clear:both; +} + +#form_settings_avatar li { +width:auto; +} +#form_settings_avatar input { +margin-left:0; +} +#avatar_original, +#avatar_preview { +float:left; +} +#avatar_preview { +margin-left:29px; +} +#avatar_preview_view { +height:96px; +width:96px; +margin-bottom:18px; +overflow:hidden; +} + +#settings_attach, +#form_settings_avatar .form_actions { +clear:both; +} + +#form_settings_avatar .form_actions { +margin-bottom:0; +} + +#form_settings_design #settings_design_color .form_data, +#form_settings_design #color-picker { +float:left; +} +#form_settings_design #settings_design_color .form_data { +width:400px; +margin-right:28px; +} + +.instructions ul { +list-style-position:inside; +} +.instructions p, +.instructions ul { +margin-bottom:18px; +} +.help dt { +display:none; +} +.guide { +clear:both; +} diff --git a/theme/biz/css/display.css b/theme/biz/css/display.css new file mode 100644 index 0000000000..a7d360c532 --- /dev/null +++ b/theme/biz/css/display.css @@ -0,0 +1,252 @@ +/** theme: biz + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +@import url(base.css); + +html { +background-color:#144A6E; +} +a:active { +background-color:#F4F7E7; +} +body { +font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; +font-size:1em; +background:#144A6E url(../images/illustrations/illu_pattern-01.png) repeat-x; +} + +address { +margin-right:7.18%; +} + +input, textarea, select, option { +font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; +} +input, textarea, select, +.entity_remote_subscribe { +border-color:#aaa; +} +#filter_tags ul li { +border-color:#ddd; +} + +.form_settings input.form_action-secondary { +background:none; +} + +input.submit, +#form_notice.warning #notice_text-count, +.form_settings .form_note, +.entity_remote_subscribe { +background-color:#9BB43E; +} + +input:focus, textarea:focus, select:focus, +#form_notice.warning #notice_data-text { +border-color:#9BB43E; +} +input.submit, +.entity_remote_subscribe, +#site_nav_local_views a { +color:#fff; +} + +a, +#site_nav_local_views .current a, +div.notice-options input, +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_send-a-message a, +.form_user_nudge input.submit, +.entity_nudge p, +.form_settings input.form_action-secondary { +color:#002E6E; +} + +#header a, +#footer a { +color:#87B4C8; +} + +.notice, +.profile { +border-top-color:#CEE1E9; +} +.section .profile { +border-top-color:#87B4C8; +} + +#content .notice p.entry-content a:visited { +background-color:#fcfcfc; +} +#content .notice p.entry-content .vcard a { +background-color:#fcfffc; +} + +.aside .section { +background-color:#F1F5F8; +background-position:100% 0; +background-image:url(../images/illustrations/illu_pattern-02.png); +background-repeat:no-repeat; +} + +#notice_text-count { +color:#333; +} +#form_notice.warning #notice_text-count { +color:#000; +} +#form_notice.processing #notice_action-submit { +background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%; +cursor:wait; +text-indent:-9999px; +} + +#content, +#site_nav_local_views a, +.aside .section { +border-color:#fff; +} +#content, +#site_nav_local_views .current a { +background-color:#fff; +} + +#site_nav_local_views a { +background-color:rgba(135, 180, 200, 0.3); +} +#site_nav_local_views a:hover { +background-color:rgba(255, 255, 255, 0.7); +} + +.error { +background-color:#F7E8E8; +} +.success { +background-color:#EFF3DC; +} + +#anon_notice { +color:#fff; +} + +#showstream #anon_notice { +} + +#export_data li a { +background-repeat:no-repeat; +background-position:0 45%; +} +#export_data li a.rss { +background-image:url(../../base/images/icons/icon_rss.png); +} +#export_data li a.atom { +background-image:url(../../base/images/icons/icon_atom.png); +} +#export_data li a.foaf { +background-image:url(../../base/images/icons/icon_foaf.gif); +} + +.entity_edit a, +.entity_send-a-message a, +.form_user_nudge input.submit, +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_nudge p { +background-position: 0 40%; +background-repeat: no-repeat; +background-color:transparent; +} +.form_group_join input.submit, +.form_group_leave input.submit +.form_user_subscribe input.submit, +.form_user_unsubscribe input.submit { +background-color:#9BB43E; +color:#fff; +} +.form_user_unsubscribe input.submit, +.form_group_leave input.submit, +.form_user_authorization input.reject { +background-color:#87B4C8; +} + +.entity_edit a { +background-image:url(../../base/images/icons/twotone/green/edit.gif); +} +.entity_send-a-message a { +background-image:url(../../base/images/icons/twotone/green/quote.gif); +} +.entity_nudge p, +.form_user_nudge input.submit { +background-image:url(../../base/images/icons/twotone/green/mail.gif); +} +.form_user_block input.submit, +.form_user_unblock input.submit { +background-image:url(../../base/images/icons/twotone/green/shield.gif); +} + +/* NOTICES */ +.notices li.over { +background-color:#fcfcfc; +} + +.notice-options .notice_reply a, +.notice-options form input.submit { +background-color:transparent; +} +.notice-options .notice_reply a { +background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%; +} +.notice-options form.form_favor input.submit { +background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%; +} +.notice-options form.form_disfavor input.submit { +background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%; +} +.notice-options .notice_delete a { +background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%; +} + +.notices div.entry-content, +.notices div.notice-options { +opacity:0.4; +} +.notices li.hover div.entry-content, +.notices li.hover div.notice-options { +opacity:1; +} +div.entry-content { +color:#333; +} +div.notice-options a, +div.notice-options input { +font-family:sans-serif; +} +.notices li.hover { +background-color:#fcfcfc; +} +/*END: NOTICES */ + +#new_group a { +background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%; +} + +.pagination .nav_prev a, +.pagination .nav_next a { +background-repeat:no-repeat; +border-color:#CEE1E9; +} +.pagination .nav_prev a { +background-image:url(../../base/images/icons/twotone/green/arrow-left.gif); +background-position:10% 45%; +} +.pagination .nav_next a { +background-image:url(../../base/images/icons/twotone/green/arrow-right.gif); +background-position:90% 45%; +} diff --git a/theme/biz/css/ie.css b/theme/biz/css/ie.css new file mode 100644 index 0000000000..2f463bb44d --- /dev/null +++ b/theme/biz/css/ie.css @@ -0,0 +1,9 @@ +/* IE specific styles */ + +.notice-options input.submit { +color:#fff; +} + +#site_nav_local_views a { +background-color:#D0DFE7; +} diff --git a/theme/biz/default-avatar-mini.png b/theme/biz/default-avatar-mini.png new file mode 100644 index 0000000000000000000000000000000000000000..38b8692b4a2f71c8de3d6a12b715df33aada5f7c GIT binary patch literal 646 zcmV;10(t$3P)t7_1ao1dG5H5=fedf?I_U zERrsbGN6ogMr}` z=geF#+zTQC5di=LaBdjJM@3P-0idfKA;f20*DnA(@I8qLjEKM(3u~J8S_pAFm&<9f zSPbEC7@TwLgOn1}=@gFRpkA-LjIlSH&E_!?ePIBYs;Zr6Ga4*xpF`v&- zEEdi`SF2Su6bfC-+-EQtpin4yM0AgV_}m^LaEH4M-{d zNe9pK002AF4@7iH=bR&(&2Ehsiv@`2laNxrAB{#2)9Ew=0L!vqS=K>6*rng^e_gNF z@3`x_^;WAT4=u}|=ytns97hAt;6zjd&?=Y9n`49wheK2$*>gXpP+!1HdC8#9K|%7P#8l;_13R g5kkBIaJK9D9f4f}@hqK`?*IS*07*qoM6N<$f=KKaB>(^b literal 0 HcmV?d00001 diff --git a/theme/biz/default-avatar-profile.png b/theme/biz/default-avatar-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..f8357d4fc296271b837b3d9911cfbc133918ef2f GIT binary patch literal 2853 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 MfY_K+7Z|aDy3W6;R?}) zJJa7D+8b}%q`4{D>!4paaL(^{&bi<3xi|OxZhl9J2i|dE0WSwJ z;8&o3y8I|2|D^3LB6AAh1ik=tKx{650`H~bDI%!ZcR(rC1bhe718ACt%jLrDcH?%t zsjjXjCnqQSb+v$ri3#rCzt6pU_lQIyjE#+%-9G>yipXCx_?iVMr9J?@266xz8X9P8 zYooHV@|6(Ign@wpIy*awMx%!5Z{TeaxosGd0+dqgn0n^&moHz={{8#ev17+#bDo2E zJkFUjXSj6flJR)|25b=#9i{~+rE-CvflW@QlM^RS;BvWaq&*i`u3X{NsZ&PtOA+}5 zK$|uk1vUZf+qdtvBM;Eh(!%D=n~mm2N~yO1G^JDl@G*eb>t*N8oim5o3i!0z3_V#k~=FJ5Y5fL6fe8}a?ml+uuu{^G}w$|L-;$Zs6JIj$0x~_BN z$PvQfu<=TxwzihNd-u}N&|sOw;NTz~9UY91j{}gClY`Ia!|(S~US2-$*ouk@%F4=k z^5hA?CJiVBC@U+=c;T+DE(Qk&F>ij6NQ91#4&w2+CGxtibMoZLBzb^DBEi+GR|y0H zmgXoeElpOn`8t~M;K2hfUc8vn7mvree*L9I7=(9lp?pRVhKLLsY^fGh+wHa6mLq>rRmtXN@bp62G}dHpL_uCz)C z$U;C-QBm59mz0#SXU`r>^K9F;jl8_Pls=!&$NKf_tx^KA5CGu!`|*0cGi(lrgF}Z7 zQCwVXX^w(|0uCQOJm-#8R#tN0z=3S&OkF>l&Z}3iroFwLTeoh}-`~%M4I8MhueZE^ z7}&mjJ6pGIB^V5%>pEVqmztWI1w{c1`aJY_Ja{~wg$zw1H#hgC#9diELa!s11lWxw z0d`|afZbRUU^kWo*p2_a087E#=;$cdu3aM*i=mWCMvY1-G)+s^&Gy*^NZD7@v^nFJ zEn7xYQdM$J zqX0X|X9Eq00_G&b4EX*2SC)~P4@#-D`H9idQKK4l0DXYx?%lg7JJ;3K5eNhbg+e@g z_RO3|Lqw8y$ZVf&OJ_H1=J`Z~f`S5kJ|9(8Rde`^jEoSA#f)a3A>e((QCL_wXHZE= z$!n3V0hqPWeHv2&0JwAK&Y}~_7U6K%sE&!qkj7NbX&m76>C-%a{@gBZ8S(h>V}ik; z(d+@xMC4zlGc-d(L!3Kz&L(>EBaujO{P=NnT?d!|K7;AksEB+E`~+~}!UYBf2JGUQ z7H7|%B@&4k%}+%n3^UWLRlseaLen&M?b?Ok@5kwM+CtbsG#aI|v(vn&-M~9CO?-Bu zyA}8zCnAEEc#v$kH_im?j{fjm~#(*06q~({y(4Us6jRG4bWoPoK7ce z)~s37qXxsn!{)#96Tnx%=OQvQlalTb1>gt9u>Y{fFF>Q^!yaZRr3!&Jd2!sP5vT!P pRW4Qse&@wen|>f9B54D%{{RsfdHbSpK`#IR002ovPDHLkV1m@7$GHFi literal 0 HcmV?d00001 diff --git a/theme/biz/images/illustrations/illu_pattern-01.png b/theme/biz/images/illustrations/illu_pattern-01.png new file mode 100644 index 0000000000000000000000000000000000000000..79bb46b60e0e05846031e864251da9aa576eb266 GIT binary patch literal 935 zcmV;Y16cftP)p(}BpEOetIV~!wSiW*RN z8dQ7~OmZx6ofS`YFLtFDQh6w7l^RlcCTEi(VvQzckt1P@FLa_VaiAh#iY#!S8BcaA zZk#T4q$p~bC1j5oPjxPJr6ppHC}xu@Zk`xTb1re7C1{l(TZAfbohxymCTElvQg|a{ zk0M};CTW%zPnh literal 0 HcmV?d00001 diff --git a/theme/biz/images/illustrations/illu_pattern-02.png b/theme/biz/images/illustrations/illu_pattern-02.png new file mode 100644 index 0000000000000000000000000000000000000000..4438b751afbf45bd4a4da6b8aafcc78373871918 GIT binary patch literal 9498 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 Date: Mon, 11 May 2009 04:04:56 +0000 Subject: [PATCH 83/83] Init pigeon thoughts theme. Inspired by http://csarven.ca/labs/csszengarden.com/pigeon-thoughts/zengarden-sample.html --- theme/pigeonthoughts/css/base.css | 1153 +++++++++++++++++ theme/pigeonthoughts/css/display.css | 295 +++++ theme/pigeonthoughts/css/ie.css | 9 + theme/pigeonthoughts/default-avatar-mini.png | Bin 0 -> 646 bytes .../pigeonthoughts/default-avatar-profile.png | Bin 0 -> 2853 bytes .../pigeonthoughts/default-avatar-stream.png | Bin 0 -> 1487 bytes .../images/illustrations/illu_pigeons-01.png | Bin 0 -> 72649 bytes .../images/illustrations/illu_pigeons-02.png | Bin 0 -> 3538 bytes theme/pigeonthoughts/logo.png | Bin 0 -> 4988 bytes 9 files changed, 1457 insertions(+) create mode 100644 theme/pigeonthoughts/css/base.css create mode 100644 theme/pigeonthoughts/css/display.css create mode 100644 theme/pigeonthoughts/css/ie.css create mode 100644 theme/pigeonthoughts/default-avatar-mini.png create mode 100644 theme/pigeonthoughts/default-avatar-profile.png create mode 100644 theme/pigeonthoughts/default-avatar-stream.png create mode 100644 theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png create mode 100644 theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png create mode 100644 theme/pigeonthoughts/logo.png diff --git a/theme/pigeonthoughts/css/base.css b/theme/pigeonthoughts/css/base.css new file mode 100644 index 0000000000..1797198200 --- /dev/null +++ b/theme/pigeonthoughts/css/base.css @@ -0,0 +1,1153 @@ +/** theme: pigeonthoughts base + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +* { margin:0; padding:0; } +img { display:block; border:0; } +a abbr { cursor: pointer; border-bottom:0; } +table { border-collapse:collapse; } +ol { list-style-position:inside; } +html { font-size: 87.5%; background-color:#fff; } +body { +background-color:#fff; +color:#000; +font-family:sans-serif; +font-size:1em; +line-height:1.65; +position:relative; +margin-left:183px; +} +h1,h2,h3,h4,h5,h6 { +margin-bottom:7px; +overflow:hidden; +} +h1 { +font-size:1.4em; +margin-bottom:18px; +} +#showstream h1 { display:none; } +h2 { font-size:1.3em; } +h3 { font-size:1.2em; } +h4 { font-size:1.1em; } +h5 { font-size:1em; } +h6 { font-size:0.9em; } + +caption { +font-weight:bold; +} +legend { +font-weight:bold; +font-size:1.3em; +} +input, textarea, select, option { +padding:4px; +font-family:sans-serif; +font-size:1em; +} +input, textarea, select { +border-width:2px; +border-style: solid; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} + +input.submit { +font-weight:bold; +cursor:pointer; +} +textarea { +overflow:auto; +} +option { +padding-bottom:0; +} +fieldset { +padding:0; +border:0; +} +form ul li { +list-style-type:none; +margin:0 0 18px 0; +} +form label { +font-weight:bold; +} +input.checkbox { +position:relative; +top:2px; +left:0; +border:0; +} + +.error, +.success { +padding:4px 1.55%; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +margin-bottom:18px; +} +form label.submit { +display:none; +} + +.form_settings { +clear:both; +} + +.form_settings fieldset { +margin-bottom:29px; +} +.form_settings input.remove { +margin-left:11px; +} +.form_settings .form_data li { +width:100%; +float:left; +} +.form_settings .form_data label { +float:left; +} +.form_settings .form_data textarea, +.form_settings .form_data select, +.form_settings .form_data input { +margin-left:11px; +float:left; +} +.form_settings .form_data input.submit { +margin-left:0; +} + +.form_settings label { +margin-top:2px; +width:152px; +} + +.form_actions label { +display:none; +} +.form_guide { +font-style:italic; +} + +.form_settings #settings_autosubscribe label { +display:inline; +font-weight:bold; +} + +#form_settings_profile legend, +#form_login legend, +#form_register legend, +#form_password legend, +#form_settings_avatar legend, +#newgroup legend, +#editgroup legend, +#form_tag_user legend, +#form_remote_subscribe legend, +#form_openid_login legend, +#form_search legend, +#form_invite legend, +#form_notice_delete legend, +#form_password_recover legend, +#form_password_change legend { +display:none; +} + +.form_settings .form_data p.form_guide { +clear:both; +margin-left:163px; +margin-bottom:0; +} + +.form_settings p { +margin-bottom:11px; +} + +.form_settings input.checkbox { +margin-top:3px; +margin-left:0; +} +.form_settings label.checkbox { +font-weight:normal; +margin-top:0; +margin-right:0; +margin-left:11px; +float:left; +width:90%; +} + + +#form_login p.form_guide, +#form_register #settings_rememberme p.form_guide, +#form_openid_login #settings_rememberme p.form_guide, +#settings_twitter_remove p.form_guide, +#form_search ul.form_data #q { +margin-left:0; +} + +.form_settings .form_note { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +padding:0 7px; +} + + +.form_settings input.form_action-secondary { +margin-left:29px; +padding:0; +} + +#form_search .submit { +margin-left:11px; +} + +address { +float:right; +margin-bottom:18px; +margin-right:18px; +} +address.vcard img.logo { +margin-right:0; +} +address .fn { +font-weight:bold; +} +address img + .fn { +display:none; +} + +#header { +width:98.5%; +position:relative; +float:left; +padding-top:18px; +padding-left:18px; +margin-bottom:29px; +} + +#site_nav_global_primary { +float:left; +margin-right:18px; +margin-bottom:11px; +} +#site_nav_global_primary ul li { +display:inline; +margin-right:11px; +} + +.system_notice dt { +font-weight:bold; +text-transform:uppercase; +display:none; +} + +#site_notice { +float:right; +margin-top:7px; +margin-right:18px; +width:26%; +} +#page_notice { +clear:both; +margin-bottom:18px; +} + + +#anon_notice { +float:left; +width:50.2%; +line-height:1.5; +font-size:1.1em; +font-weight:bold; +} + + +#footer { +float:left; +width:64%; +padding:18px; +} + +#site_nav_local_views { +width:183px; +float:left; +margin-bottom:29px; +position:fixed; +top:179px; +left:0; +} +#site_nav_local_views dt { +display:none; +} +#site_nav_local_views li { +list-style-type:none; +} +#site_nav_local_views a { +text-decoration:none; +padding:4px 11px; +text-shadow: 1px 1px 1px #ddd; +font-weight:bold; +display:block; +} +#site_nav_local_views .nav { +float:left; +width:100%; +} + +#site_nav_global_primary dt, +#site_nav_global_secondary dt { +display:none; +} + +#site_nav_global_secondary { +margin-bottom:11px; +} + +#site_nav_global_secondary ul li { +display:inline; +margin-right:11px; +} +#export_data li a { +padding-left:20px; +} +#export_data li a.foaf { +padding-left:30px; +} +#export_data li a.export_vcard { +padding-left:28px; +} + +#export_data ul { +display:inline; +} +#export_data li { +list-style-type:none; +display:inline; +margin-left:11px; +} +#export_data li:first-child { +margin-left:0; +} + +#licenses { +font-size:0.9em; +} + +#licenses dt { +font-weight:bold; +display:none; +} +#licenses dd { +margin-bottom:11px; +line-height:1.5; +} + +#site_content_license_cc { +margin-bottom:0; +} +#site_content_license_cc img { +display:inline; +vertical-align:top; +margin-right:4px; +} + +#wrap { +width:100%; +min-width:760px; +max-width:1003px; +overflow:hidden; +} + +#core { +position:relative; +width:100%; +float:left; +margin-bottom:1em; +} + +#content { +width:50.009%; +min-height:259px; +float:left; +margin-left:18px; +} +#shownotice #content { +min-height:0; +} + +#content_inner { +position:relative; +width:100%; +float:left; +} + +#aside_primary { +width:45.917%; +min-height:259px; +float:left; +margin-left:1.385%; +padding-bottom:47px; +} + +#form_notice { +width:45.664%; +float:left; +position:relative; +line-height:1; +} +#form_notice fieldset { +border:0; +padding:0; +position:relative; +} +#form_notice legend { +display:none; +} +#form_notice textarea { +float:left; +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +width:80.789%; +height:46px; +line-height:1.5; +padding:7px 7px 16px 7px; +} +#form_notice label { +display:block; +float:left; +font-size:1.3em; +margin-bottom:7px; +} +#form_notice #notice_submit label { +display:none; +} +#form_notice .form_note { +position:absolute; +top:76px; +right:98px; +z-index:9; +} +#form_notice .form_note dt { +font-weight:bold; +display:none; +} +#notice_text-count { +font-weight:bold; +line-height:1.15; +padding:1px 2px; +} +#form_notice #notice_action-submit { +width:14%; +height:47px; +padding:0; +position:absolute; +bottom:0; +right:0; +} +#form_notice label[for=to] { +margin-top:7px; +} +#form_notice select[id=to] { +margin-bottom:7px; +margin-left:18px; +float:left; +} +#form_notice .error { +float:left; +clear:both; +width:96.9%; +margin-bottom:0; +line-height:1.618; +} + +/* entity_profile */ +.entity_profile { +position:relative; +width:67.702%; +min-height:123px; +float:left; +margin-bottom:18px; +margin-left:0; +overflow:hidden; +} +.entity_profile dt, +#entity_statistics dt { +font-weight:bold; +} +.entity_profile dd { +display:inline; +} + +.entity_profile .entity_depiction { +float:left; +width:96px; +margin-right:18px; +margin-bottom:18px; +} + +.entity_profile .entity_fn, +.entity_profile .entity_nickname, +.entity_profile .entity_location, +.entity_profile .entity_url, +.entity_profile .entity_note, +.entity_profile .entity_tags { +margin-left:113px; +margin-bottom:4px; +} + +.entity_profile .entity_fn, +.entity_profile .entity_nickname { +margin-left:11px; +display:inline; +font-weight:bold; +} +.entity_profile .entity_nickname { +margin-left:0; +} + +.entity_profile .entity_fn dd:before { +content: "("; +font-weight:normal; +} +.entity_profile .entity_fn dd:after { +content: ")"; +font-weight:normal; +} + +.entity_profile dt { +display:none; +} +.entity_profile h2 { +display:none; +} +/* entity_profile */ + + +/*entity_actions*/ +.entity_actions { +float:right; +margin-left:4.35%; +max-width:25%; +} +.entity_actions h2 { +display:none; +} +.entity_actions ul { +list-style-type:none; +} +.entity_actions li { +margin-bottom:4px; +} +.entity_actions li:first-child { +border-top:0; +} +.entity_actions fieldset { +border:0; +padding:0; +} +.entity_actions legend { +display:none; +} + +.entity_actions input.submit { +display:block; +text-align:left; +width:100%; +} +.entity_actions a, +.entity_nudge p, +.entity_remote_subscribe { +text-decoration:none; +font-weight:bold; +display:block; +} + +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_send-a-message a, +.entity_edit a, +.form_user_nudge input.submit, +.entity_nudge p { +border:0; +padding-left:20px; +} + +.entity_edit a, +.entity_send-a-message a, +.entity_nudge p { +padding:4px 4px 4px 23px; +} + +.entity_remote_subscribe { +padding:4px; +border-width:2px; +border-style:solid; +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} +.entity_actions .accept { +margin-bottom:18px; +} + +.entity_tags ul { +list-style-type:none; +display:inline; +} +.entity_tags li { +display:inline; +margin-right:4px; +} + +.aside .section { +margin-bottom:29px; +float:right; +width:44%; +padding:1%; +border-width:1px; +border-style:solid; +margin-left:2.5%; +} +.aside .section h2 { +text-transform:uppercase; +font-size:1em; +} + +#entity_statistics dt, +#entity_statistics dd { +display:inline; +} +#entity_statistics dt:after { +content: ":"; +} + +.section ul.entities { +float:left; +width:100%; +} +.section .entities li { +list-style-type:none; +float:left; +margin-right:7px; +margin-bottom:7px; +} +.section .entities li .photo { +margin-right:0; +margin-bottom:0; +} +.section .entities li .fn { +display:none; +} + +.aside .section p, +.aside .section .more { +clear:both; +} + +.profile .entity_profile { +margin-bottom:0; +min-height:60px; +} + + +.profile .form_group_join legend, +.profile .form_group_leave legend, +.profile .form_user_subscribe legend, +.profile .form_user_unsubscribe legend { +display:none; +} + +.profiles { +list-style-type:none; +} +.profile .entity_profile .entity_location { +width:auto; +clear:none; +margin-left:11px; +} +.profile .entity_profile dl, +.profile .entity_profile dd { +display:inline; +float:none; +} +.profile .entity_profile .entity_note, +.profile .entity_profile .entity_url, +.profile .entity_profile .entity_tags, +.profile .entity_profile .form_subscription_edit { +margin-left:59px; +clear:none; +display:block; +width:auto; +} +.profile .entity_profile .entity_tags dt { +display:inline; +margin-right:11px; +} + + +.profile .entity_profile .form_subscription_edit label { +font-weight:normal; +margin-right:11px; +} + + +/* NOTICE */ +.notice, +.profile { +position:relative; +padding-top:11px; +padding-bottom:11px; +clear:both; +float:left; +width:96.41%; +border-width:1px; +border-style:solid; +padding:1.795%; +margin-bottom:11px; +} +.notices li { +list-style-type:none; +} + +#aside_primary .notice, +#aside_primary .profile { +border:0; +margin-bottom:11px; +} + +/* NOTICES */ +#notices_primary { +float:left; +width:100%; +border-radius:7px; +-moz-border-radius:7px; +-webkit-border-radius:7px; +} +#notices_primary h2 { +display:none; +} +.notice-data a span { +display:block; +padding-left:28px; +} + +.notice .author { +margin-right:11px; +} + +.fn { +overflow:hidden; +} + +.notice .author .fn { +font-weight:bold; +} + +.vcard .photo { +display:inline; +margin-right:11px; +float:left; +} +#shownotice .vcard .photo { +margin-bottom:4px; +} +.vcard .url { +text-decoration:none; +} +.vcard .url:hover { +text-decoration:underline; +} + +.notice .entry-title { +float:left; +width:100%; +overflow:hidden; +} +#shownotice .notice .entry-title { +font-size:2.2em; +} + +.notice p.entry-content { +display:inline; +} + +#content .notice p.entry-content a:visited { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} +.notice p.entry-content .vcard a { +border-radius:4px; +-moz-border-radius:4px; +-webkit-border-radius:4px; +} + +.notice div.entry-content { +clear:left; +float:left; +font-size:0.95em; +margin-left:59px; +width:65%; +} +#showstream .notice div.entry-content, +#shownotice .notice div.entry-content { +margin-left:0; +} + +.notice .notice-options a, +.notice .notice-options input { +float:left; +font-size:1.025em; +} + +.notice div.entry-content dl, +.notice div.entry-content dt, +.notice div.entry-content dd { +display:inline; +} + +.notice div.entry-content .timestamp dt, +.notice div.entry-content .response dt { +display:none; +} +.notice div.entry-content .timestamp a { +display:inline-block; +} +.notice div.entry-content .device dt { +text-transform:lowercase; +} + + +.notice-options { +padding-left:2%; +float:left; +width:50%; +position:relative; +font-size:0.95em; +width:12.5%; +float:right; +} + +.notice-options a { +float:left; +} +.notice-options .notice_delete, +.notice-options .notice_reply, +.notice-options .form_favor, +.notice-options .form_disfavor { +position:absolute; +top:0; +} +.notice-options .form_favor, +.notice-options .form_disfavor { +left:0; +} +.notice-options .notice_reply { +left:29px; +} +.notice-options .notice_delete { +right:0; +} +.notice-options .notice_reply dt { +display:none; +} + +.notice-options input, +.notice-options a { +text-indent:-9999px; +outline:none; +} + +.notice-options .notice_reply a, +.notice-options input.submit { +display:block; +border:0; +} +.notice-options .notice_reply a, +.notice-options .notice_delete a { +text-decoration:none; +padding-left:16px; +} + +.notice-options form input.submit { +width:16px; +padding:2px 0; +} + +.notice-options .notice_delete dt, +.notice-options .form_favor legend, +.notice-options .form_disfavor legend { +display:none; +} +.notice-options .notice_delete fieldset, +.notice-options .form_favor fieldset, +.notice-options .form_disfavor fieldset { +border:0; +padding:0; +} + + +#usergroups #new_group { +float: left; +margin-right: 2em; +} +#new_group, #group_search { +margin-bottom:18px; +} +#new_group a { +padding-left:20px; +} + + +#filter_tags { +margin-bottom:11px; +float:left; +} +#filter_tags dt { +display:none; +} +#filter_tags ul { +list-style-type:none; +} +#filter_tags ul li { +float:left; +margin-left:7px; +padding-left:7px; +border-left-width:1px; +border-left-style:solid; +} +#filter_tags ul li.child_1 { +margin-left:0; +border-left:0; +padding-left:0; +} +#filter_tags ul li#filter_tags_all a { +font-weight:bold; +margin-top:7px; +float:left; +} + +#filter_tags ul li#filter_tags_item label { +margin-right:7px; +} +#filter_tags ul li#filter_tags_item label, +#filter_tags ul li#filter_tags_item select { +display:inline; +} +#filter_tags ul li#filter_tags_item p { +float:left; +margin-left:38px; +} +#filter_tags ul li#filter_tags_item input { +position:relative; +top:3px; +left:3px; +} + + + +.pagination { +float:left; +clear:both; +width:100%; +margin-top:18px; +} + +.pagination dt { +font-weight:bold; +display:none; +} + +.pagination .nav { +float:left; +width:100%; +list-style-type:none; +} + +.pagination .nav_prev { +float:left; +} +.pagination .nav_next { +float:right; +} + +.pagination a { +display:block; +text-decoration:none; +font-weight:bold; +padding:7px; +border-width:1px; +border-style:solid; +-moz-border-radius:7px; +-webkit-border-radius:7px; +border-radius:7px; +} + +.pagination .nav_prev a { +padding-left:30px; +} +.pagination .nav_next a { +padding-right:30px; +} +/* END: NOTICE */ + + +.hentry .entry-content p { +margin-bottom:18px; +} +.system_notice ul, +.instructions ul, +.hentry entry-content ol, +.hentry .entry-content ul { +list-style-position:inside; +} +.hentry .entry-content li { +margin-bottom:18px; +} +.hentry .entry-content li li { +margin-left:18px; +} + + + + +/* TOP_POSTERS */ +.section tbody td { +padding-right:11px; +padding-bottom:11px; +} +.section .vcard .photo { +margin-right:7px; +margin-bottom:0; +} + +.section .notice { +padding-top:7px; +padding-bottom:7px; +border-top:0; +} + +.section .notice:first-child { +padding-top:0; +} + +.section .notice .author { +margin-right:0; +} +.section .notice .author .fn { +display:none; +} + + +/* tagcloud */ +.tag-cloud { +list-style-type:none; +text-align:center; +} +.aside .tag-cloud { +font-size:0.8em; +} +.tag-cloud li { +display:inline; +margin-right:7px; +line-height:1.25; +} +.aside .tag-cloud li { +line-height:1.5; +} +.tag-cloud li a { +text-decoration:none; +} +#tagcloud.section dt { +text-transform:uppercase; +font-weight:bold; +} +.tag-cloud-1 { +font-size:1em; +} +.tag-cloud-2 { +font-size:1.25em; +} +.tag-cloud-3 { +font-size:1.75em; +} +.tag-cloud-4 { +font-size:2em; +} +.tag-cloud-5 { +font-size:2.25em; +} +.tag-cloud-6 { +font-size:2.75em; +} +.tag-cloud-7 { +font-size:3.25em; +} + +#publictagcloud #tagcloud.section dt { +display:none; +} + +#form_settings_photo .form_data { +clear:both; +} + +#form_settings_avatar li { +width:auto; +} +#form_settings_avatar input { +margin-left:0; +} +#avatar_original, +#avatar_preview { +float:left; +} +#avatar_preview { +margin-left:29px; +} +#avatar_preview_view { +height:96px; +width:96px; +margin-bottom:18px; +overflow:hidden; +} + +#settings_attach, +#form_settings_avatar .form_actions { +clear:both; +} + +#form_settings_avatar .form_actions { +margin-bottom:0; +} + +#form_settings_design #settings_design_color .form_data, +#form_settings_design #color-picker { +float:left; +} +#form_settings_design #settings_design_color .form_data { +width:400px; +margin-right:28px; +} + +.instructions ul { +list-style-position:inside; +} +.instructions p, +.instructions ul { +margin-bottom:18px; +} +.help dt { +display:none; +} +.guide { +clear:both; +} diff --git a/theme/pigeonthoughts/css/display.css b/theme/pigeonthoughts/css/display.css new file mode 100644 index 0000000000..19341ef7f2 --- /dev/null +++ b/theme/pigeonthoughts/css/display.css @@ -0,0 +1,295 @@ +/** theme: pigeonthoughts + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +@import url(base.css); + +html { +background:#fff url(../images/illustrations/illu_pigeons-01.png) no-repeat 0 100%; +} + +body, +a:active { +background-color:#AEA187; +} +body { +font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; +font-size:1em; +} +address { +margin-left:2%; +} + +input, textarea, select, option { +font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; +} +input, textarea, select, +.entity_remote_subscribe { +border-color:#aaa; +} +#filter_tags ul li { +border-color:#ddd; +} + +.form_settings input.form_action-secondary { +background:none; +} + +input.submit, +#form_notice.warning #notice_text-count, +.form_settings .form_note, +.entity_remote_subscribe { +background-color:#8F0000; +} + +input:focus, textarea:focus, select:focus, +#form_notice.warning #notice_data-text { +border-color:#8F0000; +} +input.submit, +.entity_remote_subscribe { +color:#fff; +} + +a, +div.notice-options input, +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_send-a-message a, +.form_user_nudge input.submit, +.entity_nudge p, +.form_settings input.form_action-secondary { +color:#000; +} + +.notice, +.profile { +border-color:#000; +} +.notice a, +.profile a { +color:#fff; +} + +.notice:nth-child(3n-1), +.profile:nth-child(3n-1) { +border-color:#fff; +} +.notice:nth-child(3n-1) a, +.profile:nth-child(3n-1) a { +color:#7F1114; +} +.notice:nth-child(3n), +.profile:nth-child(3n) { +border-color:#7F1114; +} +.notice:nth-child(3n) a, +.profile:nth-child(3n) a { +color:#000; +} + +.aside .section .notice, +.aside .section .profile, +.aside .section .notice:nth-child(3n-1), +.aside .section .profile:nth-child(3n-1), +.aside .section .notice:nth-child(3n), +.aside .section .profile:nth-child(3n) { +background-color:transparent; +color:#000; +} + + +.aside .section { +border-color:#fff; +background-color:#fff; +color:#000; +} + +.aside .section:nth-child(n) { +border-color:#000; +background-color:#000; +color:#fff; +} +.aside .section:nth-child(3n-1) { +border-color:#fff; +background-color:#fff; +color:#000; +} +.aside .section:nth-child(3n) { +background-color:#7F1114; +border-color:#7F1114; +color:#000; +} +.aside .section a { +color:#7F1114; +} +.aside .section:nth-child(3n-1) a { +color:#7F1114; +} +.aside .section:nth-child(3n) a { +color:#fff; +} + + +.section .profile { +border-top-color:#87B4C8; +} + +#aside_primary { +background:url(../images/illustrations/illu_pigeons-02.png) no-repeat 10% 100%; +} + +#notice_text-count { +color:#333; +} +#form_notice.warning #notice_text-count { +color:#000; +} +#form_notice.processing #notice_action-submit { +background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%; +cursor:wait; +text-indent:-9999px; +} + +#content, +#site_nav_local_views a { +border-color:#fff; +} +#site_nav_local_views .current a { +background-color:rgba(143, 0, 0, 0.8); +color:#fff; +} + +#site_nav_local_views a { +background-color:rgba(255, 255, 255, 0.3); +} +#site_nav_local_views a:hover { +background-color:#fff; +color:#8F0000; +} + +.error { +background-color:#F7E8E8; +} +.success { +background-color:#EFF3DC; +} + +#anon_notice { +color:#000; +} + + +#export_data li a { +background-repeat:no-repeat; +background-position:0 45%; +} +#export_data li a.rss { +background-image:url(../../base/images/icons/icon_rss.png); +} +#export_data li a.atom { +background-image:url(../../base/images/icons/icon_atom.png); +} +#export_data li a.foaf { +background-image:url(../../base/images/icons/icon_foaf.gif); +} + +.entity_edit a, +.entity_send-a-message a, +.form_user_nudge input.submit, +.form_user_block input.submit, +.form_user_unblock input.submit, +.entity_nudge p { +background-position: 0 40%; +background-repeat: no-repeat; +background-color:transparent; +} +.form_group_join input.submit, +.form_group_leave input.submit +.form_user_subscribe input.submit, +.form_user_unsubscribe input.submit { +background-color:#8F0000; +color:#fff; +} +.form_user_unsubscribe input.submit, +.form_group_leave input.submit, +.form_user_authorization input.reject { +background-color:#87B4C8; +} + +.entity_edit a { +background-image:url(../../base/images/icons/twotone/green/edit.gif); +} +.entity_send-a-message a { +background-image:url(../../base/images/icons/twotone/green/quote.gif); +} +.entity_nudge p, +.form_user_nudge input.submit { +background-image:url(../../base/images/icons/twotone/green/mail.gif); +} +.form_user_block input.submit, +.form_user_unblock input.submit { +background-image:url(../../base/images/icons/twotone/green/shield.gif); +} + +/* NOTICES */ +.notices li.over { +background-color:#fcfcfc; +} + +.notice-options .notice_reply a, +.notice-options form input.submit { +background-color:transparent; +} +.notice-options .notice_reply a { +background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%; +} +.notice-options form.form_favor input.submit { +background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%; +} +.notice-options form.form_disfavor input.submit { +background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%; +} +.notice-options .notice_delete a { +background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%; +} + +.notices div.entry-content, +.notices div.notice-options { +opacity:0.4; +} +.notices li.hover div.entry-content, +.notices li.hover div.notice-options { +opacity:1; +} +div.entry-content { +color:#333; +} +div.notice-options a, +div.notice-options input { +font-family:sans-serif; +} +/*END: NOTICES */ + +#new_group a { +background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%; +} + +.pagination .nav_prev a, +.pagination .nav_next a { +background-repeat:no-repeat; +border-color:#000; +} +.pagination .nav_prev a { +background-image:url(../../base/images/icons/twotone/green/arrow-left.gif); +background-position:10% 45%; +} +.pagination .nav_next a { +background-image:url(../../base/images/icons/twotone/green/arrow-right.gif); +background-position:90% 45%; +} diff --git a/theme/pigeonthoughts/css/ie.css b/theme/pigeonthoughts/css/ie.css new file mode 100644 index 0000000000..2f463bb44d --- /dev/null +++ b/theme/pigeonthoughts/css/ie.css @@ -0,0 +1,9 @@ +/* IE specific styles */ + +.notice-options input.submit { +color:#fff; +} + +#site_nav_local_views a { +background-color:#D0DFE7; +} diff --git a/theme/pigeonthoughts/default-avatar-mini.png b/theme/pigeonthoughts/default-avatar-mini.png new file mode 100644 index 0000000000000000000000000000000000000000..38b8692b4a2f71c8de3d6a12b715df33aada5f7c GIT binary patch literal 646 zcmV;10(t$3P)t7_1ao1dG5H5=fedf?I_U zERrsbGN6ogMr}` z=geF#+zTQC5di=LaBdjJM@3P-0idfKA;f20*DnA(@I8qLjEKM(3u~J8S_pAFm&<9f zSPbEC7@TwLgOn1}=@gFRpkA-LjIlSH&E_!?ePIBYs;Zr6Ga4*xpF`v&- zEEdi`SF2Su6bfC-+-EQtpin4yM0AgV_}m^LaEH4M-{d zNe9pK002AF4@7iH=bR&(&2Ehsiv@`2laNxrAB{#2)9Ew=0L!vqS=K>6*rng^e_gNF z@3`x_^;WAT4=u}|=ytns97hAt;6zjd&?=Y9n`49wheK2$*>gXpP+!1HdC8#9K|%7P#8l;_13R g5kkBIaJK9D9f4f}@hqK`?*IS*07*qoM6N<$f=KKaB>(^b literal 0 HcmV?d00001 diff --git a/theme/pigeonthoughts/default-avatar-profile.png b/theme/pigeonthoughts/default-avatar-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..f8357d4fc296271b837b3d9911cfbc133918ef2f GIT binary patch literal 2853 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 MfY_K+7Z|aDy3W6;R?}) zJJa7D+8b}%q`4{D>!4paaL(^{&bi<3xi|OxZhl9J2i|dE0WSwJ z;8&o3y8I|2|D^3LB6AAh1ik=tKx{650`H~bDI%!ZcR(rC1bhe718ACt%jLrDcH?%t zsjjXjCnqQSb+v$ri3#rCzt6pU_lQIyjE#+%-9G>yipXCx_?iVMr9J?@266xz8X9P8 zYooHV@|6(Ign@wpIy*awMx%!5Z{TeaxosGd0+dqgn0n^&moHz={{8#ev17+#bDo2E zJkFUjXSj6flJR)|25b=#9i{~+rE-CvflW@QlM^RS;BvWaq&*i`u3X{NsZ&PtOA+}5 zK$|uk1vUZf+qdtvBM;Eh(!%D=n~mm2N~yO1G^JDl@G*eb>t*N8oim5o3i!0z3_V#k~=FJ5Y5fL6fe8}a?ml+uuu{^G}w$|L-;$Zs6JIj$0x~_BN z$PvQfu<=TxwzihNd-u}N&|sOw;NTz~9UY91j{}gClY`Ia!|(S~US2-$*ouk@%F4=k z^5hA?CJiVBC@U+=c;T+DE(Qk&F>ij6NQ91#4&w2+CGxtibMoZLBzb^DBEi+GR|y0H zmgXoeElpOn`8t~M;K2hfUc8vn7mvree*L9I7=(9lp?pRVhKLLsY^fGh+wHa6mLq>rRmtXN@bp62G}dHpL_uCz)C z$U;C-QBm59mz0#SXU`r>^K9F;jl8_Pls=!&$NKf_tx^KA5CGu!`|*0cGi(lrgF}Z7 zQCwVXX^w(|0uCQOJm-#8R#tN0z=3S&OkF>l&Z}3iroFwLTeoh}-`~%M4I8MhueZE^ z7}&mjJ6pGIB^V5%>pEVqmztWI1w{c1`aJY_Ja{~wg$zw1H#hgC#9diELa!s11lWxw z0d`|afZbRUU^kWo*p2_a087E#=;$cdu3aM*i=mWCMvY1-G)+s^&Gy*^NZD7@v^nFJ zEn7xYQdM$J zqX0X|X9Eq00_G&b4EX*2SC)~P4@#-D`H9idQKK4l0DXYx?%lg7JJ;3K5eNhbg+e@g z_RO3|Lqw8y$ZVf&OJ_H1=J`Z~f`S5kJ|9(8Rde`^jEoSA#f)a3A>e((QCL_wXHZE= z$!n3V0hqPWeHv2&0JwAK&Y}~_7U6K%sE&!qkj7NbX&m76>C-%a{@gBZ8S(h>V}ik; z(d+@xMC4zlGc-d(L!3Kz&L(>EBaujO{P=NnT?d!|K7;AksEB+E`~+~}!UYBf2JGUQ z7H7|%B@&4k%}+%n3^UWLRlseaLen&M?b?Ok@5kwM+CtbsG#aI|v(vn&-M~9CO?-Bu zyA}8zCnAEEc#v$kH_im?j{fjm~#(*06q~({y(4Us6jRG4bWoPoK7ce z)~s37qXxsn!{)#96Tnx%=OQvQlalTb1>gt9u>Y{fFF>Q^!yaZRr3!&Jd2!sP5vT!P pRW4Qse&@wen|>f9B54D%{{RsfdHbSpK`#IR002ovPDHLkV1m@7$GHFi literal 0 HcmV?d00001 diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png new file mode 100644 index 0000000000000000000000000000000000000000..4fdaaeb25b461d3300852feaa9252778ce2699ca GIT binary patch literal 72649 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 bBmS3@lgK4WNY0sm*&lscED%< literal 0 HcmV?d00001 diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png new file mode 100644 index 0000000000000000000000000000000000000000..187c6c8a660390e37c9eace9e3bfa39d36676ef0 GIT binary patch literal 3538 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 Ju2!^+_z$X!Irjhn literal 0 HcmV?d00001 diff --git a/theme/pigeonthoughts/logo.png b/theme/pigeonthoughts/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7c68b34f61008753d12394806c3cf46f7b04effd GIT binary patch literal 4988 zcmV-?6NBuDP)Px|Hc3Q5RCwC$oq2o|Rkp{!x4W}L5<`$3kq~eY0bx``WDyV{Ac}z7_y!ea=FL+9 zmj}LizNj-SBj7SV9dJR>LB&zT1Y|%75M+@>0YOAr5?K;3kdTnItM2?!6-k=zs!G#I zDw+CyKK1z|)%V`Ib?aB>o^$TGrv!}%G~PPKfgqXM3~ zKV|*G#y*`iNJ1=JKCq=%m9HV>$U44srWESL2U9lecL-qd(To_nn!+7 zHYW@Aaq7$gsG+_nkrN7{bHk+Mp2nhV+FnAuzP$e=DfwGX>O_Ra(z)Gml3EWTF|MWFU(=%uNlx|I?<7dA-TRP z6?Px|fbHMEfhxULs^kmd={YfFO_pP2)B?QW>P%~75cEo(z)e>_7cw#cxP+S?Np~@_ z?_Y_ENU%~RUd6k>kr1_j`OEuVgVZPaFvm%1%k~Co6sHd-=n)P6mM3=++FsdM=US@#d<85OY&A26`Mqgi1Q34eSSE zh*`PL?MBeG!HlmCOJ(XGr>n=2PAAK23=(tbClA z&!J^P585`ry59Qec7@X``CfD7Qk8c(gt$y5 z04!ND&J7GSDia;ij5hTs9nR@F9y+xiYG!|SS-5;)ONS6a;&e=IS0K))O#2pnaS69A z`g>`s0j6cVs?yIPM6lBZ{K~9M>%`u+8MtP#$%K1ijI9lqxXd9$u!NAZi&>e3rb#x^ zcU&}!OExZ~%FYfUf>$V;nff1M+SqFF;$n=KT7*lqcL)(Aq$JAd_b^Y4ZS;pqM1;kf zNIfDQLIlkSni&1;afjP(@WMP%CbvYiLx|v&$_Qf#V!sVk72Xmvl6xIO1S=q1oHY8q zw7dXCt+3VLl~oj&Y03o-A%dPUIBHg=s3g}`gI93&i19rMSH44tV5baKm=fY-{$Bg2 zJW}OU;X#v9>VAh1!4kr4K6p8&cG_m(P8A-Yto)4e;7YOAAw-ZUrpY$IP)p0s-GnOb zu_xwmPNumOQRLSRjS#kqseCQ1{}%{uFe)>o_p@|p*}vYWiw`pHIH%J{Q1%v}e3Os* zLm|r15;$EhWvL>7v%t>+4ocXrLPjBco(ko5B!tIi^jrhRgvdUzson{3@OTDACAr8# zFBNG=saTzZJXsd#Ws3n~BwPXva=~M9kpDcq4ZJi34%+Dv*eV`Eku1SvKi$RgUF)@v zs`RpB-$Kei`ibKEwotw}%O4rc5Dz>8eD?y(nGH>yDI?Idk$(MZqafQmZdgK-sQTOd zy=m=SwmtYX$`6I1Z1#YFyC=hzP&S&n^y~PDup$xTDlC3@#E7znNr<%T$0Eds{(ehn z+L1B+7uH+f_O4pYs@r}GMc&$YGfRO9li`D4+nBdC~ytuB##&P5#$!B z%7xQMUAtZgAsYlCUj1xf2alp2^7mVhq`SH5vIpy>UxzaP$*03cB1>x38?ES)qDG`h z&B%z1ERK9yO7RC>XEEirkW893s?P>n>kt58suFOzXxsd%dg;^gJzGh;d30@)0l+O` z(TiX?(rU(v&0QnTMs1f`?u;0V)w)W-x>?IN&6ZLMTN6UX<##JqxF?1d30Ks4e@^Co z&qrg1q2`y>)g{AK_~2zQjSrczd`oAc@&yo#rtOkaJTY_mmRGBi!d7PI{B6C%M2_?; zA=)&*3YVzM?n?G%EhF=($MNnzZMjxl$Brb89?eBvx)7O=KzUIS`TO^iwQ3b7cI>d) zOzR4m0|bospS^5DQ&q}Uzy$^J3YauAee-@`p6zsqH_`_TQju%?{ibc|t(&BCxu*|?Dn)24AWGt&|yRk-p|@awOinZEh| zf&UDVN?RsW(R

      SMRZvgiw(wfq2}mc^~7^lPWoRszU(w|PVLgpr603IhU9VM7`A8;o=D>WsaAdaGBzWFjeqx{NNsLP-F|G@-F>ML=#2|z~QOYSQJ;9l?N60TIdW z)FH>rUa`5YZ3rgt0?5B6Pq}klgW{jU*T1ZZ?xcSHU($1IdJTY zn%RH5^N|I{0XcC=Neq4SP0M7citA>~;K=&*9N)OnP{CASH1K+LX(+}Uv$>ez6H+8b&seEl7+j4Dc zD&b9o^g0W{fO+$HzfT{tl0&N!V#bQ~&4nrh?ng+7iDcYWU2waq8<4#&X+c488Cwnp zmU=?c&o+dR$do|uS;p7T)OmlD{l>28$bljn?}RA ztY^|?=oBgU298cYQ&NtiszzeE&=!PH;X)Hdl|YCy$*RW91+6>yAR^v8Z&gGU2~k>l zHVgcR_lJAjT-GXZ#=I>DkC|QSTYI>!1vds!D^DQaLdaVRiz}Kbk}kkp;8+NHlqcN6 zP;)KK%L~b^msn^=aPF%jz9~NdkEN#aMC)Jo?;`|5kz5b-#C%;K zAJ`2t(zHzQjv#&)`2RV+P5zx_y6-w)4*XR0)Bate@@%f3xVH_(c2E?u#tW2wSLb#-2GF^y0AU^?h>BN)`JIs=dYv0IPzY*AY z&c`t@5tQ~Ww}=;tj8%nD!BvbZ3#1T-CXHJAvu``k!#*QIWUpRLMNtu9O)fz7k&?qJ zSDKYNG6{Zhh`5EAE-f`JIeNM#K|J!UspI>W)H@-hlsAg=R0_5TDL(Ncm;P?lx*sZk zcp%7Y=nwqo8dlqZL42pTq=elI7SL=%sNmp17F~Y1 zRWe8y`~|f23k0_d*dY;1ByxsCOc1EodBT(y(?|Acicp6O_{KuJMIc|6uJS8<$>2x` zD|!w*E#N7u<=x@ov>!5r5lfa37E?XMann;zvE|ukt&;KGsc;WKYDR`9b3;JPc(5ZUbth^nUlwJM4M$&2!VrrcIW}0F*0mSACGd zA%sOthVOw}fa03e%XsJ^^7ib}2=eZ`*0g?@^r-foF&skJ7*k*^+9I4~2bZ_FnC0Wf zQI?<2{v}JyikAT6r@&kFd7q9BQA0cr9f6O~)<`U&S+{QFefzCZk%Pc*rog89zCVW$ zL0~?FmB3`FSsxb?$KeHDh=%{0RKw}WA%qR_99$d*4@&qQ&?bn%UpyJ^w$nX1ga`pr z!4n5pNw`VCAF!|uPxmzpp9(7-LNsE`g%K`Dw=lLR3ZP9Y_DXK*SQ#P4qp(7oO-!Z3 z`*tIPLx@^J0sIf}rAe{B+p+J?lu=7~5!$O-kc~fm;`=FJ%a5x#5~7yy2xJSGfS+h^ z0gLV0Z-)@Ih{=#HwLA~i)CURgI)wN)MU19TRP{h^wp(sWhY)p$Nl>A{oxmyGx_H;l z{SB2;`2tk?MAA}h=5VQR%!5$^{-Hpd$7&t{UZ+R|K1R#QyblN_%sXFKo+^)NjqG_w zqixAA0Dc03%HA>;Ml^(*{n+PWmH}NHLR7p`JWm&A$I9XUghz=NqK)`bfmEk&P`}*@s*-5JqYWgp-mZCKK)HM zDc=Lgt5SYD*0aVoD0p;hpexWHI1YSHsBt6@b43xt-&i~?WE4@FJSVi@)`^&1_Z=fVfoNlghc$KqfHBE&_y^G*0@Q!n;ywIVZ4#oj0VAyvVnz+>54S*w zlO}Dv-zVc{kfe`(I9PcMAj)CfDD(|FElUC}N0yX{?l~wcdTF%cU zu??82zjapN>)9&@{tUeAr>qsnSRzn}mJ$9@KU%-Py_hfP57&3I>puN_<&jg1dfT;r z#^D9dkDwC3^?qCgZl80T^(o{>pYsuZoQ{^Q3^PRdIX3~=gSMl|%7-ALeExqO_@n-J zSE{G)vAWmcZQxb??|#6oLEKwi!&f(008G*UF4dL%Bf4VvoljYR<8q5`0d4c6l*iT< zA^f2-qgAl^`GR7w4~nY%U1Bouc12Z-U}BKwFrkua_4yXvuOGFAXdKiSPJoO9+enDb z1`e3b&ZDsxA)+mK;VPBHxFE0nO%?OBFL0^ur^un~T|pGbVMahUwo4 zbU;hboTDr2W{E@Mb*m}9HVF~t$29WA-_YF6R==$3+s3`X0bMD52B@0C?RkAL8xne= zEu*`u=tv5hd$}rna}57pe-WaA|Mk&*CZc&QsbYor;w}BUQ)`nDzVMgaV4!{g>_>CQ z8X*(t?PFyG+VZxk0ZNCVetZP}Lgsb>qpJKL1H2fZP2X>U@Agw>w0?k=?>qTIZo;Zk zxewT2@cUmeUoxL%(KPxJEv&Dh!QV9JFEjs3Sj!)^*w2f&4%ll@XKr0u8TaCU6>Te; zbLfBtdwH-w?lk z3p5X3S3m45@>9MA@VG7>C0a1epLC_>CbabifUgLgL^A>{NyZpmk*v@~;d{Vm29l-; zaF-v&^eg@E8_<0Hl_(-`{K8#S_xfdHyltQ-aJA35PyI+dk51@BG_T=dgEnJz9U%p{ z1Z|=2Yh7nBOFGsXZB66`ef&PtC2$cil5<)7H<(D2^YAatj^1`Jt