summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Prodromou <evan@controlyourself.ca>2009-05-11 17:53:23 -0400
committerEvan Prodromou <evan@controlyourself.ca>2009-05-11 17:53:23 -0400
commit99f8501d321ae9d8087abbe6523e28ce5fd095d3 (patch)
treefb721b4360f0748df3cb3b522565ad4566422555
parent7f0e57598a5837bbb87f197baa147be29c7df531 (diff)
parent251fd2c232d96ea09db971a6608231c679395c01 (diff)
Merge branch '0.8.x' of git@gitorious.org:laconica/dev into 0.8.x
Conflicts: config.php.sample
-rw-r--r--.gitignore3
-rw-r--r--actions/accesstoken.php2
-rw-r--r--actions/all.php2
-rw-r--r--actions/api.php1
-rw-r--r--actions/favoritesrss.php2
-rw-r--r--actions/newmessage.php45
-rw-r--r--actions/openidsettings.php4
-rw-r--r--actions/recoverpassword.php4
-rw-r--r--actions/showfavorites.php4
-rw-r--r--actions/twitapifriendships.php6
-rw-r--r--actions/twitapistatuses.php38
-rw-r--r--actions/twitapiusers.php28
-rw-r--r--actions/twittersettings.php25
-rw-r--r--classes/Fave.php53
-rw-r--r--classes/Foreign_link.php2
-rw-r--r--classes/Notice.php241
-rw-r--r--classes/Notice_inbox.php51
-rw-r--r--classes/Notice_tag.php61
-rw-r--r--classes/Profile.php53
-rw-r--r--classes/Reply.php47
-rw-r--r--classes/User.php68
-rwxr-xr-xclasses/User_group.php53
-rwxr-xr-xclasses/laconica.ini2
-rw-r--r--db/laconica.sql2
-rw-r--r--db/laconica_pg.sql2
-rw-r--r--db/notice_source.sql3
-rw-r--r--extlib/DB/DataObject.php17
-rw-r--r--install.php161
-rw-r--r--js/identica-badge.js2
-rw-r--r--js/util.js35
-rw-r--r--lib/common.php4
-rw-r--r--lib/galleryaction.php4
-rw-r--r--lib/router.php4
-rw-r--r--lib/subpeopletagcloudsection.php1
-rw-r--r--lib/subscriberspeopleselftagcloudsection.php10
-rw-r--r--lib/subscriberspeopletagcloudsection.php5
-rw-r--r--lib/subscriptionspeopleselftagcloudsection.php9
-rw-r--r--lib/subscriptionspeopletagcloudsection.php4
-rw-r--r--lib/twitterapi.php29
-rw-r--r--lib/util.php12
-rw-r--r--plugins/Comet/CometPlugin.php205
-rw-r--r--plugins/Comet/README26
-rw-r--r--plugins/Comet/bayeux.class.inc.php134
-rw-r--r--plugins/Comet/jquery.comet.js1451
-rw-r--r--plugins/Comet/json2.js478
-rw-r--r--plugins/Comet/updatetimeline.js154
-rw-r--r--plugins/LinkbackPlugin.php8
-rwxr-xr-xscripts/getvaliddaemons.php55
-rwxr-xr-xscripts/inboxqueuehandler.php69
-rwxr-xr-xscripts/memcachedqueuehandler.php70
-rwxr-xr-xscripts/startdaemons.sh5
-rw-r--r--scripts/statusfetcher.php525
-rwxr-xr-xscripts/stopdaemons.sh3
-rwxr-xr-xscripts/synctwitterfriends.php38
-rwxr-xr-xscripts/twitterstatusfetcher.php576
-rwxr-xr-xscripts/xmppdaemon.php17
-rw-r--r--theme/base/css/display.css54
-rw-r--r--theme/biz/css/base.css1170
-rw-r--r--theme/biz/css/display.css252
-rw-r--r--theme/biz/css/ie.css9
-rw-r--r--theme/biz/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/biz/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/biz/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/biz/images/illustrations/illu_pattern-01.pngbin0 -> 935 bytes
-rw-r--r--theme/biz/images/illustrations/illu_pattern-02.pngbin0 -> 9498 bytes
-rw-r--r--theme/biz/logo.pngbin0 -> 4988 bytes
-rw-r--r--theme/default/css/display.css10
-rw-r--r--theme/default/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/pigeonthoughts/css/base.css1153
-rw-r--r--theme/pigeonthoughts/css/display.css295
-rw-r--r--theme/pigeonthoughts/css/ie.css9
-rw-r--r--theme/pigeonthoughts/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/pigeonthoughts/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/pigeonthoughts/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/pigeonthoughts/images/illustrations/illu_pigeons-01.pngbin0 -> 72649 bytes
-rw-r--r--theme/pigeonthoughts/images/illustrations/illu_pigeons-02.pngbin0 -> 3538 bytes
-rw-r--r--theme/pigeonthoughts/logo.pngbin0 -> 4988 bytes
77 files changed, 7015 insertions, 850 deletions
diff --git a/.gitignore b/.gitignore
index 83a53dfa3..da6947bfd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ dataobject.ini
*.rej
.#*
*.swp
+.buildpath
+.project
+.settings
diff --git a/actions/accesstoken.php b/actions/accesstoken.php
index bb68d3314..46b43c702 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__);
diff --git a/actions/all.php b/actions/all.php
index a92e55462..a53bbea07 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);
}
diff --git a/actions/api.php b/actions/api.php
index d2f0a2eff..8762b4bcd 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');
diff --git a/actions/favoritesrss.php b/actions/favoritesrss.php
index f85bf1b19..6b46b8dec 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/newmessage.php b/actions/newmessage.php
index 82276ff34..52d4899ba 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();
}
diff --git a/actions/openidsettings.php b/actions/openidsettings.php
index 92469d20f..5f59ebc01 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 620fe7eb8..82263fcd5 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');
diff --git a/actions/showfavorites.php b/actions/showfavorites.php
index 6e011d5ca..eed62a2ab 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);
}
diff --git a/actions/twitapifriendships.php b/actions/twitapifriendships.php
index c50c5e84a..2f8250e0d 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/twitapistatuses.php b/actions/twitapistatuses.php
index 323c4f1f8..3abeba367 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/actions/twitapiusers.php b/actions/twitapiusers.php
index 2894b7486..1542cfb33 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,24 +103,16 @@ 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));
+ // 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';
- }
- }
- }
+ if ($sub) {
+ $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
+ }
+ }
if ($apidata['content-type'] == 'xml') {
$this->init_document('xml');
diff --git a/actions/twittersettings.php b/actions/twittersettings.php
index 580d9ecf7..2b742788e 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) {
@@ -383,6 +392,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/classes/Fave.php b/classes/Fave.php
index 24df5938c..915b4572f 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/Foreign_link.php b/classes/Foreign_link.php
index 5d9c82a85..606560951 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/Notice.php b/classes/Notice.php
index a399d97e0..382d160ab 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -206,7 +206,12 @@ class Notice extends Memcached_DataObject
$notice->saveTags();
$notice->saveGroups();
- $notice->addToInboxes();
+ if (common_config('queue', 'enabled')) {
+ $notice->addToAuthorInbox();
+ } else {
+ $notice->addToInboxes();
+ }
+
$notice->query('COMMIT');
Event::handle('EndNoticeSave', array($notice));
@@ -216,7 +221,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('queue', 'enabled')) {
+ $notice->blowAuthorCaches();
+ } else {
+ $notice->blowCaches();
+ }
}
return $notice;
@@ -280,6 +289,17 @@ class Notice extends Memcached_DataObject
$this->blowGroupCache($blowLast);
}
+ function blowAuthorCaches($blowLast=false)
+ {
+ // Clear the user's cache
+ $cache = common_memcache();
+ if (!empty($cache)) {
+ $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id));
+ }
+ $this->blowNoticeCache($blowLast);
+ $this->blowPublicCache($blowLast);
+ }
+
function blowGroupCache($blowLast=false)
{
$cache = common_memcache();
@@ -288,17 +308,17 @@ 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;
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'));
}
}
}
@@ -317,10 +337,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();
@@ -341,9 +358,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();
@@ -355,10 +372,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'));
}
}
}
@@ -372,9 +389,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'));
}
}
}
@@ -404,9 +421,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'));
}
}
}
@@ -602,27 +619,80 @@ 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)
{
+ $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
+ array(),
+ 'public',
+ $offset, $limit, $since_id, $before_id, $since);
+
+ return Notice::getStreamByIds($ids);
+ }
+
+ function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
+ {
+ $notice = new Notice();
+
+ $notice->selectAdd(); // clears it
+ $notice->selectAdd('id');
- $parts = array();
+ $notice->orderBy('id DESC');
- $qry = 'SELECT * FROM notice ';
+ 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()
@@ -648,6 +718,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');
@@ -700,24 +797,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
@@ -913,4 +1015,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 = $cache->get($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(',', $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 81ddb4538..dec14b0d1 100644
--- a/classes/Notice_inbox.php
+++ b/classes/Notice_inbox.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -21,7 +21,11 @@ if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Notice_inbox extends Memcached_DataObject
+// We keep 5 pages of inbox notices in memcache, +1 for pagination check
+
+define('INBOX_CACHE_WINDOW', 101);
+
+class Notice_inbox extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -38,4 +42,47 @@ 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, $limit, $since_id, $before_id, $since)
+ {
+ 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)
+ {
+ $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/Notice_tag.php b/classes/Notice_tag.php
index f2247299a..e5b772243 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 _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()
+ 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');
+ }
}
}
diff --git a/classes/Profile.php b/classes/Profile.php
index f3bfe299c..ae5641d79 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 ';
-
- return Notice::getStream(sprintf($qry, $this->id),
- 'profile:notices:'.$this->id,
- $offset, $limit, $since_id, $before_id);
+ // 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::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)
diff --git a/classes/Reply.php b/classes/Reply.php
index af86aaf87..4439053b4 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 3b9b5cd83..b5ac7b220 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);
@@ -401,13 +402,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)
@@ -422,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)
@@ -444,21 +436,17 @@ 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;
- }
- return Notice::getStream(sprintf($qry, $this->id),
- 'user:notices_with_friends:' . $this->id,
- $offset, $limit, $since_id, $before_id,
- $order, $since);
+ $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+
+ return Notice::getStreamByIds($ids);
+ }
}
function blowFavesCache()
@@ -467,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'));
}
}
diff --git a/classes/User_group.php b/classes/User_group.php
index d152f9d56..7cc31e702 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));
diff --git a/classes/laconica.ini b/classes/laconica.ini
index dd424bbdd..5a905a4bb 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 83d610f0d..d9e21a7b5 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',
diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql
index f879d7936..a27a616f2 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' */,
diff --git a/db/notice_source.sql b/db/notice_source.sql
index d5124e223..f026679d5 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()),
@@ -23,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()),
@@ -42,6 +44,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()),
diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php
index b1a1a4e21..0c6a13dc2 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'])) {
diff --git a/install.php b/install.php
index 87a99a650..66e8e8712 100644
--- a/install.php
+++ b/install.php
@@ -52,23 +52,21 @@ function checkPrereqs()
foreach ($reqs as $req) {
if (!checkExtension($req)) {
- ?><p class="error">Cannot load required extension &quot;<?php echo $req; ?>&quot;.</p><?php
+ ?><p class="error">Cannot load required extension: <code><?php echo $req; ?></code></p><?php
return false;
}
}
if (!is_writable(INSTALLDIR)) {
- ?><p class="error">Cannot write config file to &quot;<?php echo INSTALLDIR; ?>&quot;.</p>
- <p>On your server, try this command:</p>
- <blockquote>chmod a+w <?php echo INSTALLDIR; ?></blockquote>
+ ?><p class="error">Cannot write config file to: <code><?php echo INSTALLDIR; ?></code></p>
+ <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?></code>
<?php
return false;
}
if (!is_writable(INSTALLDIR.'/avatar/')) {
- ?><p class="error">Cannot write avatar directory &quot;<?php echo INSTALLDIR; ?>/avatar/&quot;.</p>
- <p>On your server, try this command:</p>
- <blockquote>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</blockquote>
+ ?><p class="error">Cannot write avatar directory: <code><?php echo INSTALLDIR; ?>/avatar/</code></p>
+ <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</code></p>
<?
return false;
}
@@ -89,65 +87,78 @@ function checkExtension($name)
function showForm()
{
?>
-<p>Enter your database connection information below to initialize the database.</p>
-<form method='post' action='install.php'>
- <fieldset>
- <ul class='form_data'>
- <li>
- <label for='sitename'>Site name</label>
- <input type='text' id='sitename' name='sitename' />
- <p>The name of your site</p>
- </li>
- <li>
- <li>
- <label for='host'>Hostname</label>
- <input type='text' id='host' name='host' />
- <p>Database hostname</p>
- </li>
- <li>
- <label for='host'>Database</label>
- <input type='text' id='database' name='database' />
- <p>Database name</p>
- </li>
- <li>
- <label for='username'>Username</label>
- <input type='text' id='username' name='username' />
- <p>Database username</p>
- </li>
- <li>
- <label for='password'>Password</label>
- <input type='password' id='password' name='password' />
- <p>Database password</p>
- </li>
- </ul>
- <input type='submit' name='submit' value='Submit'>
- </fieldset>
+ </ul>
+ </dd>
+</dl>
+<dl id="page_notice" class="system_notice">
+ <dt>Page notice</dt>
+ <dd>
+ <div class="instructions">
+ <p>Enter your database connection information below to initialize the database.</p>
+ </div>
+ </dd>
+</dl>
+<form method="post" action="install.php" class="form_settings" id="form_install">
+ <fieldset>
+ <legend>Connection settings</legend>
+ <ul class="form_data">
+ <li>
+ <label for="sitename">Site name</label>
+ <input type="text" id="sitename" name="sitename" />
+ <p class="form_guide">The name of your site</p>
+ </li>
+ <li>
+ <li>
+ <label for="host">Hostname</label>
+ <input type="text" id="host" name="host" />
+ <p class="form_guide">Database hostname</p>
+ </li>
+ <li>
+ <label for="host">Database</label>
+ <input type="text" id="database" name="database" />
+ <p class="form_guide">Database name</p>
+ </li>
+ <li>
+ <label for="username">Username</label>
+ <input type="text" id="username" name="username" />
+ <p class="form_guide">Database username</p>
+ </li>
+ <li>
+ <label for="password">Password</label>
+ <input type="password" id="password" name="password" />
+ <p class="form_guide">Database password</p>
+ </li>
+ </ul>
+ <input type="submit" name="submit" class="submit" value="Submit" />
+ </fieldset>
</form>
-<?
+<?php
}
function updateStatus($status, $error=false)
{
?>
- <li>
-<?
- print $status;
-?>
- </li>
-<?
+ <li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
+
+<?php
}
function handlePost()
{
?>
- <ul>
-<?
+
+<?php
$host = $_POST['host'];
$database = $_POST['database'];
$username = $_POST['username'];
$password = $_POST['password'];
$sitename = $_POST['sitename'];
-
+?>
+ <dl class="system_notice">
+ <dt>Page notice</dt>
+ <dd>
+ <ul>
+<?php
if (empty($host)) {
updateStatus("No hostname specified.", true);
showForm();
@@ -222,8 +233,8 @@ function handlePost()
}
updateStatus("Done!");
?>
- </ul>
-<?
+
+<?php
}
function writeConf($sitename, $sqlUrl)
@@ -253,21 +264,35 @@ function runDbScript($filename, $conn)
}
?>
-<html>
-<head>
- <title>Install Laconica</title>
- <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.7.1" media="screen, projection, tv"/>
- <link rel="stylesheet" type="text/css" href="theme/base/css/modal.css?version=0.7.1" media="screen, projection, tv"/>
- <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.7.1" media="screen, projection, tv"/>
-</head>
-<body>
- <div id="wrap">
- <div id="core">
- <div id="content">
- <h1>Install Laconica</h1>
+<?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
+<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
+ <head>
+ <title>Install Laconica</title>
+ <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.8" media="screen, projection, tv"/>
+ <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.8" media="screen, projection, tv"/>
+ <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css?version=0.8" /><![endif]-->
+ <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css?version=0.8" /><![endif]-->
+ <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/earthy/css/ie.css?version=0.8" /><![endif]-->
+ </head>
+ <body id="install">
+ <div id="wrap">
+ <div id="header">
+ <address id="site_contact" class="vcard">
+ <a class="url home bookmark" href=".">
+ <img class="logo photo" src="theme/default/logo.png" alt="Laconica"/>
+ <span class="fn org">Laconica</span>
+ </a>
+ </address>
+ </div>
+ <div id="core">
+ <div id="content">
+ <h1>Install Laconica</h1>
<?php main(); ?>
- </div>
- </div>
- </div>
-</body>
+ </div>
+ </div>
+ </div>
+ </body>
</html>
diff --git a/js/identica-badge.js b/js/identica-badge.js
index 869230b7a..ffa55ae93 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();
diff --git a/js/util.js b/js/util.js
index 53e6eb792..3f14bc61c 100644
--- a/js/util.js
+++ b/js/util.js
@@ -166,6 +166,22 @@ $(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 (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 {
+ 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) {
var result = document._importNode($("p", xml).get(0), true);
result = result.textContent || result.innerHTML;
@@ -178,11 +194,14 @@ $(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);
+ 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();
@@ -192,10 +211,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();
});
diff --git a/lib/common.php b/lib/common.php
index 3f8123c51..3feba1d8b 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
@@ -193,7 +195,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/galleryaction.php b/lib/galleryaction.php
index 0484918ce..8fa11a756 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
+}
diff --git a/lib/router.php b/lib/router.php
index 5e16f3419..9308c818a 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -237,12 +237,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
diff --git a/lib/subpeopletagcloudsection.php b/lib/subpeopletagcloudsection.php
index d98f28afa..9f6948dc9 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 b5a39c6de..115241a53 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 23011efdd..4dafbc1c7 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 8ac65adb0..3896294d2 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 c3f7d1763..f9c8672e3 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';
}
}
+
diff --git a/lib/twitterapi.php b/lib/twitterapi.php
index 6a90b4e28..caf8c0716 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) {
diff --git a/lib/util.php b/lib/util.php
index da009682b..198185338 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');
@@ -1019,7 +1024,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);
@@ -1385,11 +1390,10 @@ function common_compatible_license($from, $to)
*/
function common_database_tablename($tablename)
{
-
+
if(common_config('db','quote_identifiers')) {
$tablename = '"'. $tablename .'"';
}
//table prefixes could be added here later
return $tablename;
}
-
diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php
new file mode 100644
index 000000000..45251c66f
--- /dev/null
+++ b/plugins/Comet/CometPlugin.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to do "real time" updates using Comet/Bayeux
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @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 <evan@controlyourself.ca>
+ * @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, $username=null, $password=null)
+ {
+ $this->server = $server;
+ $this->username = $username;
+ $this->password = $password;
+
+ parent::__construct();
+ }
+
+ function onEndShowScripts($action)
+ {
+ $timeline = null;
+
+ $this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action'));
+
+ switch ($action->trimmed('action')) {
+ 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;
+ }
+
+ $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)),
+ ' ');
+ }
+
+ $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\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });");
+ $action->elementEnd('script');
+
+ 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';
+ }
+
+ $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');
+
+ $json = $this->noticeAsJson($notice);
+
+ // Bayeux? Comet? Huh? These terms confuse me
+ $bay = new Bayeux($this->server, $this->user, $this->password);
+
+ foreach ($timelines as $timeline) {
+ $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'.");
+ $bay->publish($timeline, $json);
+ }
+
+ $bay = NULL;
+ }
+
+ 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);
+ $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;
+
+ 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)
+ {
+ common_log($level, get_class($this) . ': '.$msg);
+ }
+}
diff --git a/plugins/Comet/README b/plugins/Comet/README
new file mode 100644
index 000000000..4abd40af7
--- /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.
diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php
new file mode 100644
index 000000000..39ad8a8fc
--- /dev/null
+++ b/plugins/Comet/bayeux.class.inc.php
@@ -0,0 +1,134 @@
+<?php
+/*
+ *
+ * Phomet: a php comet client
+ *
+ * Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> 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;
+
+ private $sUser = '';
+ private $sPassword = '';
+
+ public $sUrl = '';
+
+ function __construct($sUrl, $sUser='', $sPassword='')
+ {
+ $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);
+
+ if (!is_null($sUser) && mb_strlen($sUser) > 0) {
+ curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword");
+ }
+
+ $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);
+
+ 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/jquery.comet.js b/plugins/Comet/jquery.comet.js
new file mode 100644
index 000000000..6de437fa8
--- /dev/null
+++ b/plugins/Comet/jquery.comet.js
@@ -0,0 +1,1451 @@
+/**
+ * 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 <code>$.cometd</code>,
+ * 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:
+ * <pre>
+ * var url2 = ...;
+ * var cometd2 = new $.Cometd();
+ * cometd2.init(url2);
+ * </pre>
+ */
+ $.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<channel, subscription[]>, 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:
+ * <pre>
+ * {
+ * 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.
+ * </pre>
+ * @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/json2.js b/plugins/Comet/json2.js
new file mode 100644
index 000000000..7e27df518
--- /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 '&nbsp;'),
+ 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
new file mode 100644
index 000000000..170949e9b
--- /dev/null
+++ b/plugins/Comet/updatetimeline.js
@@ -0,0 +1,154 @@
+// update the local timeline from a Comet server
+//
+
+var updater = function()
+{
+ var _server;
+ var _timeline;
+ var _userid;
+ var _replyurl;
+ var _favorurl;
+ var _deleteurl;
+ var _cometd;
+
+ return {
+ 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);
+ }
+ }
+
+ function leave()
+ {
+ _cometd.disconnect();
+ }
+
+ 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"});
+ $("#notices_primary .notice:first").fadeIn(1000);
+ NoticeHover();
+ NoticeReply();
+ }
+
+ function makeNoticeItem(data)
+ {
+ user = data['user'];
+ html = data['html'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"');
+ source = data['source'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"');
+
+ ni = "<li class=\"hentry notice\" id=\"notice-"+data['id']+"\">"+
+ "<div class=\"entry-title\">"+
+ "<span class=\"vcard author\">"+
+ "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+ "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
+ "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
+ "</a>"+
+ "</span>"+
+ "<p class=\"entry-content\">"+html+"</p>"+
+ "</div>"+
+ "<div class=\"entry-content\">"+
+ "<dl class=\"timestamp\">"+
+ "<dt>Published</dt>"+
+ "<dd>"+
+ "<a rel=\"bookmark\" href=\""+data['url']+"\" >"+
+ "<abbr class=\"published\" title=\""+data['created_at']+"\">a few seconds ago</abbr>"+
+ "</a> "+
+ "</dd>"+
+ "</dl>"+
+ "<dl class=\"device\">"+
+ "<dt>From</dt> "+
+ "<dd>"+source+"</dd>"+ // may have a link, I think
+ "</dl>";
+
+ if (data['in_reply_to_status_id']) {
+ ni = ni+" <dl class=\"response\">"+
+ "<dt>To</dt>"+
+ "<dd>"+
+ "<a href=\""+data['in_reply_to_status_url']+"\" rel=\"in-reply-to\">in reply to</a>"+
+ "</dd>"+
+ "</dl>";
+ }
+
+ ni = ni+"</div>"+
+ "<div class=\"notice-options\">";
+
+ 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+"</div>"+
+ "</li>";
+ return ni;
+ }
+
+ function makeFavoriteForm(id, session_key)
+ {
+ var ff;
+
+ ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+_favorurl+"\">"+
+ "<fieldset>"+
+ "<legend>Favor this notice</legend>"+ // XXX: i18n
+ "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
+ "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
+ "<input type=\"submit\" id=\"favor-submit-"+id+"\" name=\"favor-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Favor this notice\"/>"+
+ "</fieldset>"+
+ "</form>";
+ return ff;
+ }
+
+ function makeReplyLink(id, nickname)
+ {
+ var rl;
+ rl = "<dl class=\"notice_reply\">"+
+ "<dt>Reply to this notice</dt>"+
+ "<dd>"+
+ "<a href=\""+_replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span>"+
+ "</a>"+
+ "</dd>"+
+ "</dl>";
+ return rl;
+ }
+
+ function makeDeleteLink(id)
+ {
+ var dl, delurl;
+ delurl = _deleteurl.replace("0000000000", id);
+
+ dl = "<dl class=\"notice_delete\">"+
+ "<dt>Delete this notice</dt>"+
+ "<dd>"+
+ "<a href=\""+delurl+"\" title=\"Delete this notice\">Delete</a>"+
+ "</dd>"+
+ "</dl>";
+
+ return dl;
+ }
+}();
+
diff --git a/plugins/LinkbackPlugin.php b/plugins/LinkbackPlugin.php
index 881ead99e..93a0294c4 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 <ran@blogish.org>, GPL2
+ // Ran Aroussi <ran@blogish.org>, GPL2 or any later version
// http://phptrackback.sourceforge.net/
function getTrackback($text, $url)
diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php
new file mode 100755
index 000000000..a10233e69
--- /dev/null
+++ b/scripts/getvaliddaemons.php
@@ -0,0 +1,55 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * 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 ";
+}
+if(common_config('twitterbridge','enabled')) {
+ echo "twitterstatusfetcher.php ";
+}
+echo "ombqueuehandler.php ";
+echo "twitterqueuehandler.php ";
+echo "facebookqueuehandler.php ";
+echo "pingqueuehandler.php ";
+echo "inboxqueuehandler.php ";
+echo "smsqueuehandler.php ";
diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php
new file mode 100755
index 000000000..73d31e854
--- /dev/null
+++ b/scripts/inboxqueuehandler.php
@@ -0,0 +1,69 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// 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');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class InboxQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'inbox';
+ }
+
+ function start() {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ $this->log(LOG_INFO, "Distributing notice to inboxes for $notice->id");
+ $notice->addToInboxes();
+ $notice->blowSubsCache();
+ return true;
+ }
+
+ function finish() {
+ }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new InboxQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php
new file mode 100755
index 000000000..185b781f7
--- /dev/null
+++ b/scripts/memcachedqueuehandler.php
@@ -0,0 +1,70 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// 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');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class MemcachedQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'memcache';
+ }
+
+ function start() {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ // XXX: fork here
+ $this->log(LOG_INFO, "Blowing memcached for $notice->id");
+ $notice->blowCaches();
+ return true;
+ }
+
+ function finish() {
+ }
+
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new MemcachedQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh
index c3729761d..3869e95c4 100755
--- a/scripts/startdaemons.sh
+++ b/scripts/startdaemons.sh
@@ -21,10 +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; do
+for f in $DAEMONS; do
echo -n "Starting $f...";
php $DIR/$f
diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php
deleted file mode 100644
index 8f4b60cf7..000000000
--- a/scripts/statusfetcher.php
+++ /dev/null
@@ -1,525 +0,0 @@
-#!/usr/bin/env php
-<?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-// 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);
-
-// 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);
-
- foreach ($flink_ids as $f){
-
- $pid = pcntl_fork();
-
- if ($pid == -1) {
- die ("Couldn't fork!");
- }
-
- // Parent
- if ($pid) {
- if (defined('SCRIPT_DEBUG')) {
- print "Parent: forked " . $pid . "\n";
- }
- $children[] = $pid;
- } else {
-
- // Child
-
- // XXX: Each child needs its own DB connection
-
- getTimeline($f);
- 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";
- }
- remove_ps($children, $c);
- }
- }
- }
-
- // 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
- if (defined('SCRIPT_DEBUG')) {
- print "Waiting $POLL_INTERVAL secs before hitting Twitter again.\n";
- }
-
- sleep($POLL_INTERVAL);
-
-} while (true);
-
-
-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($fid)
-{
-
- // XXX: Need to reconnect to the DB here?
-
- $flink = Foreign_link::getByForeignID($fid, 1);
- $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";
- }
- }
-
- $screenname = $fuser->nickname;
-
- $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);
- }
-
-}
-
-function saveStatus($status, $flink)
-{
- // Do we have a profile for this Twitter user?
-
- $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__);
- 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 (!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
- 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 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");
- $profile->free();
- unset($profile);
-
- saveAvatars($user, $id);
-
- return $id;
- }
-}
-
-function checkAvatar($user, $profile)
-{
- $path_parts = pathinfo($user->profile_image_url);
- $newname = 'Twitter_' . $user->id . '_' .
- $path_parts['basename'];
-
- $oldname = $profile->getAvatar(48)->filename;
-
- 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";
- }
-
- $img_root = substr($path_parts['basename'], 0, -11);
- $ext = $path_parts['extension'];
- $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)) {
- 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';
- }
-
- return $mediatype;
-}
-
-function saveAvatars($user, $id)
-{
- $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 (defined('SCRIPT_DEBUG')) {
- print "Problem fetching Avatar: $url\n";
- }
- }
- }
-}
-
-function updateAvatar($profile_id, $size, $mediatype, $filename) {
-
- 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";
- }
- return;
- }
-
- $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
- $avatar = $profile->getAvatar($sizes[$size]);
-
- if ($avatar) {
- common_debug("Deleting $size avatar for $profile->nickname.");
- @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
- $avatar->delete();
- }
-
- newAvatar($profile->id, $size, $mediatype, $filename);
-}
-
-function newAvatar($profile_id, $size, $mediatype, $filename)
-{
- $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");
-
- $avatar->created = common_sql_now();
-
- $id = $avatar->insert();
-
- 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.");
-
- return $id;
-}
-
-function fetchAvatar($url, $filename)
-{
- $avatar_dir = INSTALLDIR . '/avatar/';
-
- $avatarfile = $avatar_dir . $filename;
-
- $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";
- }
-
- $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;
-}
diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh
index 2bb8f9ecb..764037e8f 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 ; do
+ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
+ memcachehandler inboxhandler twitterstatusfetcher; do
FILES="$DIR/$f.*.pid"
for ff in "$FILES" ; do
diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php
index 794301f0f..bd08ba58d 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);
diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php
new file mode 100755
index 000000000..9dfadc760
--- /dev/null
+++ b/scripts/twitterstatusfetcher.php
@@ -0,0 +1,576 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// 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);
+
+// Tune number of processes and how often to poll Twitter
+// XXX: Should these things be in config.php?
+define('MAXCHILDREN', 2);
+define('POLL_INTERVAL', 60); // in seconds
+
+// Uncomment this to get useful logging
+define('SCRIPT_DEBUG', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/daemon.php');
+
+class TwitterStatusFetcher extends Daemon
+{
+
+ private $children = array();
+
+ function name()
+ {
+ return ('twitterstatusfetcher.generic');
+ }
+
+ function run()
+ {
+ do {
+
+ $flinks = $this->refreshFlinks();
+
+ foreach ($flinks as $f){
+
+ // We have to disconnect from the DB before forking so
+ // each sub-process will open its own connection and
+ // avoid stomping on the others
+
+ $conn = &$f->getDatabaseConnection();
+ $conn->disconnect();
+
+ $pid = pcntl_fork();
+
+ if ($pid == -1) {
+ die ("Couldn't fork!");
+ }
+
+ if ($pid) {
+
+ // Parent
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Parent: forked new status fetcher process " . $pid);
+ }
+
+ $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) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Child $c finished.");
+ }
+
+ $this->remove_ps($this->children, $c);
+ }
+
+ // Wait! We have too many damn kids.
+ if (sizeof($this->children) > MAXCHILDREN) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Too many children. Waiting...');
+ }
+
+ if (($c = pcntl_wait($status, WUNTRACED)) > 0){
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Finished waiting for $c");
+ }
+
+ $this->remove_ps($this->children, $c);
+ }
+ }
+ }
+
+ // Remove all children from the process list before restarting
+ while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Child $c finished.");
+ }
+
+ $this->remove_ps($this->children, $c);
+ }
+
+ // Rest for a bit before we fetch more statuses
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Waiting ' . POLL_INTERVAL .
+ ' secs before hitting Twitter again.');
+ }
+
+ if (POLL_INTERVAL > 0) {
+ 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')) {
+ common_debug('Updating Twitter friends subscriptions' .
+ " for $cnt users.");
+ }
+
+ $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");
+ return;
+ }
+
+ $fuser = $flink->getForeignUser();
+
+ if (empty($fuser)) {
+ common_log(LOG_WARNING, "Unmatched user for ID " .
+ $flink->user_id);
+ return;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ 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,
+ $flink->credentials);
+
+ $timeline = json_decode($timeline_json);
+
+ if (empty($timeline)) {
+ common_log(LOG_WARNING, "Empty timeline.");
+ return;
+ }
+
+ foreach ($timeline as $status) {
+
+ // 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;
+ }
+
+ $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.');
+ return null;
+ }
+
+ $uri = 'http://twitter.com/' . $status->user->screen_name .
+ '/status/' . $status->id;
+
+ $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')) {
+ common_debug('Could not save notice!');
+ }
+ }
+
+ // XXX: Figure out a better way to link Twitter replies?
+ $notice->saveReplies();
+
+ // XXX: Do we want to pollute our tag cloud with
+ // hashtags from Twitter?
+ $notice->saveTags();
+ $notice->saveGroups();
+
+ $notice->query('COMMIT');
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Saved status $status->id" .
+ " as notice $notice->id.");
+ }
+ }
+
+ 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) {
+ 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 {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Adding profile and remote profile ' .
+ "for Twitter user: $profileurl");
+ }
+
+ $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__);
+ $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__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+ }
+
+ $profile->query("COMMIT");
+
+ $this->saveAvatars($user, $id);
+
+ return $id;
+ }
+ }
+
+ function checkAvatar($user, $profile)
+ {
+ global $config;
+
+ $path_parts = pathinfo($user->profile_image_url);
+ $newname = 'Twitter_' . $user->id . '_' .
+ $path_parts['basename'];
+
+ $oldname = $profile->getAvatar(48)->filename;
+
+ if ($newname != $oldname) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Avatar for Twitter user ' .
+ "$profile->nickname has changed.");
+ common_debug("old: $oldname new: $newname");
+ }
+
+ $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';
+ }
+
+ return $mediatype;
+ }
+
+ 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 = $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->newAvatar($id, $size, $mediatype, $filename);
+ } else {
+ common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
+ }
+ }
+ }
+
+ function updateAvatar($profile_id, $size, $mediatype, $filename) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Updating avatar: $size");
+ }
+
+ $profile = Profile::staticGet($profile_id);
+
+ if (!$profile) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Couldn't get profile: $profile_id!");
+ }
+ return;
+ }
+
+ $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
+ $avatar = $profile->getAvatar($sizes[$size]);
+
+ if ($avatar) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Deleting $size avatar for $profile->nickname.");
+ }
+ @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
+ $avatar->delete();
+ }
+
+ $this->newAvatar($profile->id, $size, $mediatype, $filename);
+ }
+
+ function newAvatar($profile_id, $size, $mediatype, $filename)
+ {
+ global $config;
+
+ $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);
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("new filename: $avatar->url");
+ }
+
+ $avatar->created = common_sql_now();
+
+ $id = $avatar->insert();
+
+ if (!$id) {
+ common_log_db_error($avatar, 'INSERT', __FILE__);
+ return null;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Saved new $size avatar for $profile_id.");
+ }
+
+ return $id;
+ }
+
+ function fetchAvatar($url, $filename)
+ {
+ $avatar_dir = INSTALLDIR . '/avatar/';
+
+ $avatarfile = $avatar_dir . $filename;
+
+ $out = fopen($avatarfile, 'wb');
+ if (!$out) {
+ common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
+ return false;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Fetching avatar: $url");
+ }
+
+ $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();
+
diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php
index 0ce2f2a28..b79fa1b3b 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);
- }
- else {
- $content = $body;
+ $from = jabber_normalize_jid($pl['from']);
+ $this->from_site($from, "Message too long - maximum is 140 characters, you sent ".mb_strlen($content_shortened));
+ return;
}
- $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;
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index a6fe8eb75..10fc63638 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;
@@ -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;
@@ -426,6 +429,7 @@ line-height:1;
#form_notice fieldset {
border:0;
padding:0;
+position:relative;
}
#form_notice legend {
display:none;
@@ -480,7 +484,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 {
@@ -759,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;
}
@@ -805,9 +813,10 @@ clear:left;
float:left;
font-size:0.95em;
margin-left:59px;
-width:70%;
+width:65%;
}
-#showstream .notice div.entry-content {
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
margin-left:0;
}
@@ -835,23 +844,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;
@@ -1029,6 +1021,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;
diff --git a/theme/biz/css/base.css b/theme/biz/css/base.css
new file mode 100644
index 000000000..22bbced08
--- /dev/null
+++ b/theme/biz/css/base.css
@@ -0,0 +1,1170 @@
+/** theme: biz base
+ *
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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 000000000..a7d360c53
--- /dev/null
+++ b/theme/biz/css/display.css
@@ -0,0 +1,252 @@
+/** theme: biz
+ *
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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 000000000..2f463bb44
--- /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 000000000..38b8692b4
--- /dev/null
+++ b/theme/biz/default-avatar-mini.png
Binary files differ
diff --git a/theme/biz/default-avatar-profile.png b/theme/biz/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/biz/default-avatar-profile.png
Binary files differ
diff --git a/theme/biz/default-avatar-stream.png b/theme/biz/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/biz/default-avatar-stream.png
Binary files differ
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 000000000..79bb46b60
--- /dev/null
+++ b/theme/biz/images/illustrations/illu_pattern-01.png
Binary files differ
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 000000000..4438b751a
--- /dev/null
+++ b/theme/biz/images/illustrations/illu_pattern-02.png
Binary files differ
diff --git a/theme/biz/logo.png b/theme/biz/logo.png
new file mode 100644
index 000000000..7c68b34f6
--- /dev/null
+++ b/theme/biz/logo.png
Binary files differ
diff --git a/theme/default/css/display.css b/theme/default/css/display.css
index 0c8fae166..1fc99eff7 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 {
diff --git a/theme/default/logo.png b/theme/default/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/default/logo.png
Binary files differ
diff --git a/theme/pigeonthoughts/css/base.css b/theme/pigeonthoughts/css/base.css
new file mode 100644
index 000000000..179719820
--- /dev/null
+++ b/theme/pigeonthoughts/css/base.css
@@ -0,0 +1,1153 @@
+/** theme: pigeonthoughts base
+ *
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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 000000000..19341ef7f
--- /dev/null
+++ b/theme/pigeonthoughts/css/display.css
@@ -0,0 +1,295 @@
+/** theme: pigeonthoughts
+ *
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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 000000000..2f463bb44
--- /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 000000000..38b8692b4
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-mini.png
Binary files differ
diff --git a/theme/pigeonthoughts/default-avatar-profile.png b/theme/pigeonthoughts/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-profile.png
Binary files differ
diff --git a/theme/pigeonthoughts/default-avatar-stream.png b/theme/pigeonthoughts/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-stream.png
Binary files differ
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 000000000..4fdaaeb25
--- /dev/null
+++ b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png
Binary files differ
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 000000000..187c6c8a6
--- /dev/null
+++ b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png
Binary files differ
diff --git a/theme/pigeonthoughts/logo.png b/theme/pigeonthoughts/logo.png
new file mode 100644
index 000000000..7c68b34f6
--- /dev/null
+++ b/theme/pigeonthoughts/logo.png
Binary files differ