From 71ecd689019a8086570c677af47ead4e02227fb3 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 16 Feb 2010 12:45:00 -0500 Subject: add a FIXME to Profile --- classes/Profile.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index ab05bb854..c79b1d893 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -807,6 +807,8 @@ class Profile extends Memcached_DataObject null, 'http://activitystrea.ms/schema/1.0/person' ); + // FIXME: this presupposes a local user -- not necessarily the case + // instead use User::uri or Remote_profile::uri or Ostatus_profile::homeuri $xs->element( 'id', null, -- cgit v1.2.3-54-g00ecf From eea52c708b4688c9b39f24d3931edc9da2cf1b07 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 11:32:10 -0800 Subject: Add rel="avatar" to img links in stanzas --- classes/Profile.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index c79b1d893..8f578c95a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -807,8 +807,6 @@ class Profile extends Memcached_DataObject null, 'http://activitystrea.ms/schema/1.0/person' ); - // FIXME: this presupposes a local user -- not necessarily the case - // instead use User::uri or Remote_profile::uri or Ostatus_profile::homeuri $xs->element( 'id', null, @@ -824,6 +822,7 @@ class Profile extends Memcached_DataObject $xs->element( 'link', array( 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, + 'rel' => 'avatar', 'href' => empty($avatar) ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) : $avatar->displayUrl() -- cgit v1.2.3-54-g00ecf From c892726c80b4e466b2bbad0f7b396cf0c7a137d9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 16:22:58 -0800 Subject: Take remote profiles into account when looking up canonical profile URIs --- EVENTS.txt | 5 ++++- classes/Notice.php | 2 +- classes/Profile.php | 21 +++++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) (limited to 'classes/Profile.php') diff --git a/EVENTS.txt b/EVENTS.txt index 69fe2ddcc..f333c5442 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1,4 +1,4 @@ -\InitializePlugin: a chance to initialize a plugin in a complete environment +InitializePlugin: a chance to initialize a plugin in a complete environment CleanupPlugin: a chance to cleanup a plugin at the end of a program @@ -722,3 +722,6 @@ StartRobotsTxt: Before outputting the robots.txt page EndRobotsTxt: After the default robots.txt page (good place for customization) - &$action: RobotstxtAction being shown +GetProfileUri: When determining the canonical URI for a given profile +- &$profile: the current profile + diff --git a/classes/Notice.php b/classes/Notice.php index 73b22d58a..f184b9c52 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1036,7 +1036,7 @@ class Notice extends Memcached_DataObject $xs->element( 'link', array( 'rel' => 'ostatus:attention', - 'href' => $profile->getAcctUri() + 'href' => $profile->getUri() ) ); } diff --git a/classes/Profile.php b/classes/Profile.php index 8f578c95a..5a86619fd 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -810,10 +810,7 @@ class Profile extends Memcached_DataObject $xs->element( 'id', null, - common_local_url( - 'userbyid', - array('id' => $this->id) - ) + $this->getUri() ); $xs->element('title', null, $this->getBestName()); @@ -835,9 +832,21 @@ class Profile extends Memcached_DataObject return $xs->getString(); } - function getAcctUri() + function getUri() { - return $this->nickname . '@' . common_config('site', 'server'); + if (Event::handle('GetProfileUri', array($this))) { + + $remote = Remote_profile::staticGet('id', $this->id); + + if (!empty($remote)) { + return $remote->uri; + } else { + return common_local_url( + 'userbyid', + array('id' => $this->id) + ); + } + } } } -- cgit v1.2.3-54-g00ecf From 2cb243808c2c1540f2690bff5a2d9932fa428923 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 16 Feb 2010 20:13:39 -0800 Subject: More sensical profile::getUri() --- EVENTS.txt | 8 ++++++-- classes/Profile.php | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) (limited to 'classes/Profile.php') diff --git a/EVENTS.txt b/EVENTS.txt index f333c5442..90242fa13 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -722,6 +722,10 @@ StartRobotsTxt: Before outputting the robots.txt page EndRobotsTxt: After the default robots.txt page (good place for customization) - &$action: RobotstxtAction being shown -GetProfileUri: When determining the canonical URI for a given profile -- &$profile: the current profile +StartGetProfileUri: When determining the canonical URI for a given profile +- $profile: the current profile +- &$uri: the URI +EndGetProfileUri: After determining the canonical URI for a given profile +- $profile: the current profile +- &$uri: the URI diff --git a/classes/Profile.php b/classes/Profile.php index 5a86619fd..494c697e4 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -769,7 +769,7 @@ class Profile extends Memcached_DataObject $xs->elementStart('author'); $xs->element('name', null, $this->nickname); - $xs->element('uri', null, $this->profileurl); + $xs->element('uri', null, $this->getUri()); $xs->elementEnd('author'); return $xs->getString(); @@ -832,21 +832,40 @@ class Profile extends Memcached_DataObject return $xs->getString(); } + /** + * Returns the best URI for a profile. Plugins may override. + * + * @return string $uri + */ function getUri() { - if (Event::handle('GetProfileUri', array($this))) { + $uri = null; - $remote = Remote_profile::staticGet('id', $this->id); + // check for a local user first + $user = User::staticGet('id', $this->id); - if (!empty($remote)) { - return $remote->uri; - } else { - return common_local_url( - 'userbyid', - array('id' => $this->id) - ); + if (!empty($user)) { + $uri = common_local_url( + 'userbyid', + array('id' => $user->id) + ); + } else { + + // give plugins a chance to set the URI + if (Event::handle('StartGetProfileUri', array($this, &$uri))) { + + // return OMB profile if any + $remote = Remote_profile::staticGet('id', $this->id); + + if (!empty($remote)) { + $uri = $remote->uri; + } + + Event::handle('EndGetProfileUri', array($this, &$uri)); } } + + return $uri; } } -- cgit v1.2.3-54-g00ecf From 52e8aa798a23b2832a748189b42c3bc77d65c9c7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 19 Feb 2010 08:16:45 -0500 Subject: Refactor subs_* functions for remote use The subs_* functions in subs.php have made a lot of assumptions about users versus profiles. I've refactored the functions to be methods of the Subscription class instead, and to use Profile objects throughout. Some of the checks for blocks or existing subscriptions depended on users or profiles, so I've moved those methods around a bit. I've left stubs for the subs_* functions until we get time to replace them. --- classes/Profile.php | 12 ++++ classes/Subscription.php | 154 +++++++++++++++++++++++++++++++++++++++++++++-- classes/User.php | 19 +----- lib/subs.php | 112 ++-------------------------------- 4 files changed, 170 insertions(+), 127 deletions(-) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index 494c697e4..6b396c8c3 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -868,4 +868,16 @@ class Profile extends Memcached_DataObject return $uri; } + function hasBlocked($other) + { + $block = Profile_block::get($this->id, $other->id); + + if (empty($block)) { + $result = false; + } else { + $result = true; + } + + return $result; + } } diff --git a/classes/Subscription.php b/classes/Subscription.php index faf1331cd..d6fb3fcbd 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -24,7 +24,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Subscription extends Memcached_DataObject +class Subscription extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -34,8 +34,8 @@ class Subscription extends Memcached_DataObject public $subscribed; // int(4) primary_key not_null public $jabber; // tinyint(1) default_1 public $sms; // tinyint(1) default_1 - public $token; // varchar(255) - public $secret; // varchar(255) + public $token; // varchar(255) + public $secret; // varchar(255) public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -45,9 +45,155 @@ class Subscription extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - + function pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Subscription', $kv); } + + /** + * Make a new subscription + * + * @param Profile $subscriber party to receive new notices + * @param Profile $other party sending notices; publisher + * + * @return Subscription new subscription + */ + + static function start($subscriber, $other) + { + if (!$subscriber->hasRight(Right::SUBSCRIBE)) { + throw new Exception(_('You have been banned from subscribing.')); + } + + if (self::exists($subscriber, $other)) { + throw new Exception(_('Already subscribed!')); + } + + if ($other->hasBlocked($subscriber)) { + throw new Exception(_('User has blocked you.')); + } + + if (Event::handle('StartSubscribe', array($subscriber, $other))) { + + $sub = new Subscription(); + + $sub->subscriber = $subscriber->id; + $sub->subscribed = $other->id; + $sub->created = common_sql_now(); + + $result = $sub->insert(); + + if (!$result) { + common_log_db_error($sub, 'INSERT', __FILE__); + throw new Exception(_('Could not save subscription.')); + } + + $sub->notify(); + + self::blow('user:notices_with_friends:%d', $subscriber->id); + + $subscriber->blowSubscriptionsCount(); + $other->blowSubscribersCount(); + + $otherUser = User::staticGet('id', $other->id); + + if (!empty($otherUser) && + $otherUser->autosubscribe && + !self::exists($other, $subscriber) && + !$subscriber->hasBlocked($other)) { + + $auto = new Subscription(); + + $auto->subscriber = $subscriber->id; + $auto->subscribed = $other->id; + $auto->created = common_sql_now(); + + $result = $auto->insert(); + + if (!$result) { + common_log_db_error($auto, 'INSERT', __FILE__); + throw new Exception(_('Could not save subscription.')); + } + + $auto->notify(); + } + + Event::handle('EndSubscribe', array($subscriber, $other)); + } + + return true; + } + + function notify() + { + # XXX: add other notifications (Jabber, SMS) here + # XXX: queue this and handle it offline + # XXX: Whatever happens, do it in Twitter-like API, too + + $this->notifyEmail(); + } + + function notifyEmail() + { + $subscribedUser = User::staticGet('id', $this->subscribed); + + if (!empty($subscribedUser)) { + + $subscriber = Profile::staticGet('id', $this->subscriber); + + mail_subscribe_notify_profile($subscribedUser, $subscriber); + } + } + + /** + * Cancel a subscription + * + */ + + function cancel($subscriber, $other) + { + if (!self::exists($subscriber, $other)) { + throw new Exception(_('Not subscribed!')); + } + + // Don't allow deleting self subs + + if ($subscriber->id == $other->id) { + throw new Exception(_('Couldn\'t delete self-subscription.')); + } + + if (Event::handle('StartUnsubscribe', array($subscriber, $other))) { + + $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + + // note we checked for existence above + + assert(!empty($sub)); + + $result = $sub->delete(); + + if (!$result) { + common_log_db_error($sub, 'DELETE', __FILE__); + throw new Exception(_('Couldn\'t delete subscription.')); + } + + self::blow('user:notices_with_friends:%d', $subscriber->id); + + $subscriber->blowSubscriptionsCount(); + $other->blowSubscribersCount(); + + Event::handle('EndUnsubscribe', array($subscriber, $other)); + } + + return; + } + + function exists($subscriber, $other) + { + $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + return (empty($sub)) ? false : true; + } } diff --git a/classes/User.php b/classes/User.php index 72c3f39e9..10b1f4865 100644 --- a/classes/User.php +++ b/classes/User.php @@ -80,11 +80,7 @@ class User extends Memcached_DataObject function isSubscribed($other) { - assert(!is_null($other)); - // XXX: cache results of this query - $sub = Subscription::pkeyGet(array('subscriber' => $this->id, - 'subscribed' => $other->id)); - return (is_null($sub)) ? false : true; + return Subscription::exists($this->getProfile(), $other); } // 'update' won't write key columns, so we have to do it ourselves. @@ -167,17 +163,8 @@ class User extends Memcached_DataObject function hasBlocked($other) { - - $block = Profile_block::get($this->id, $other->id); - - if (is_null($block)) { - $result = false; - } else { - $result = true; - $block->free(); - } - - return $result; + $profile = $this->getProfile(); + return $profile->hasBlocked($other); } /** diff --git a/lib/subs.php b/lib/subs.php index 5ac1a75a5..5376e21bd 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -19,8 +19,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once('XMPPHP/XMPP.php'); - /* Subscribe $user to nickname $other_nickname Returns true or an error message. */ @@ -44,72 +42,12 @@ function subs_subscribe_user($user, $other_nickname) function subs_subscribe_to($user, $other) { - if (!$user->hasRight(Right::SUBSCRIBE)) { - return _('You have been banned from subscribing.'); - } - - if ($user->isSubscribed($other)) { - return _('Already subscribed!'); - } - - if ($other->hasBlocked($user)) { - return _('User has blocked you.'); - } - try { - if (Event::handle('StartSubscribe', array($user, $other))) { - - if (!$user->subscribeTo($other)) { - return _('Could not subscribe.'); - return; - } - - subs_notify($other, $user); - - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); - } - - $profile = $user->getProfile(); - - $profile->blowSubscriptionsCount(); - $other->blowSubscribersCount(); - - if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) { - if (!$other->subscribeTo($user)) { - return _('Could not subscribe other to you.'); - } - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $other->id)); - } - - subs_notify($user, $other); - } - - Event::handle('EndSubscribe', array($user, $other)); - } + Subscription::start($user->getProfile(), $other); + return true; } catch (Exception $e) { return $e->getMessage(); } - - return true; -} - -function subs_notify($listenee, $listener) -{ - # XXX: add other notifications (Jabber, SMS) here - # XXX: queue this and handle it offline - # XXX: Whatever happens, do it in Twitter-like API, too - subs_notify_email($listenee, $listener); -} - -function subs_notify_email($listenee, $listener) -{ - mail_subscribe_notify($listenee, $listener); } /* Unsubscribe $user from nickname $other_nickname @@ -128,52 +66,12 @@ function subs_unsubscribe_user($user, $other_nickname) return subs_unsubscribe_to($user, $other->getProfile()); } -/* Unsubscribe user $user from profile $other - * NB: other can be a remote user. */ - function subs_unsubscribe_to($user, $other) { - if (!$user->isSubscribed($other)) - return _('Not subscribed!'); - - // Don't allow deleting self subs - - if ($user->id == $other->id) { - return _('Couldn\'t delete self-subscription.'); - } - try { - if (Event::handle('StartUnsubscribe', array($user, $other))) { - - $sub = DB_DataObject::factory('subscription'); - - $sub->subscriber = $user->id; - $sub->subscribed = $other->id; - - $sub->find(true); - - // note we checked for existence above - - if (!$sub->delete()) - return _('Couldn\'t delete subscription.'); - - $cache = common_memcache(); - - if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); - } - - $profile = $user->getProfile(); - - $profile->blowSubscriptionsCount(); - $other->blowSubscribersCount(); - - Event::handle('EndUnsubscribe', array($user, $other)); - } + Subscription::cancel($user->getProfile(), $other); + return true; } catch (Exception $e) { return $e->getMessage(); } - - return true; -} - +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf From a745d38d6d1ff336898b24decb54549c72ad1f99 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 22:52:27 -0500 Subject: slight rearrangement of getting profile URIs --- classes/Profile.php | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index 6b396c8c3..5ff746e30 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -841,28 +841,22 @@ class Profile extends Memcached_DataObject { $uri = null; - // check for a local user first - $user = User::staticGet('id', $this->id); + // give plugins a chance to set the URI + if (Event::handle('StartGetProfileUri', array($this, &$uri))) { - if (!empty($user)) { - $uri = common_local_url( - 'userbyid', - array('id' => $user->id) - ); - } else { - - // give plugins a chance to set the URI - if (Event::handle('StartGetProfileUri', array($this, &$uri))) { + // check for a local user first + $user = User::staticGet('id', $this->id); + if (!empty($user)) { + $uri = $user->uri; + } else { // return OMB profile if any $remote = Remote_profile::staticGet('id', $this->id); - if (!empty($remote)) { $uri = $remote->uri; } - - Event::handle('EndGetProfileUri', array($this, &$uri)); } + Event::handle('EndGetProfileUri', array($this, &$uri)); } return $uri; -- cgit v1.2.3-54-g00ecf From 47300a2ae9a51108fbf59a57cf5ab6e8867b54a6 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 22 Feb 2010 01:21:34 -0800 Subject: Upgrade profile-based activity noun to have more complete set of profile fields --- classes/Profile.php | 45 +++++++++++++++++++++++++++++++++++++++++++-- lib/atom10feed.php | 4 ++-- lib/atomusernoticefeed.php | 3 +-- 3 files changed, 46 insertions(+), 6 deletions(-) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index 6b396c8c3..4f67fc0bc 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -792,9 +792,11 @@ class Profile extends Memcached_DataObject * Returns an XML string fragment with profile information as an * Activity Streams noun object with the given element type. * - * Assumes that 'activity' namespace has been previously defined. + * Assumes that 'activity', 'georss', and 'poco' namespace has been + * previously defined. * * @param string $element one of 'actor', 'subject', 'object', 'target' + * * @return string */ function asActivityNoun($element) @@ -811,9 +813,46 @@ class Profile extends Memcached_DataObject 'id', null, $this->getUri() - ); + ); + + // title should contain fullname $xs->element('title', null, $this->getBestName()); + // Portable Contacts stuff + + if (isset($this->bio)) { + + // XXX: Possible to use OpenSocial's aboutMe? + + $xs->element('poco:note', null, $this->bio); + } + + if (isset($this->homepage)) { + + $xs->elementStart('poco:urls'); + $xs->element('poco:value', null, $this->homepage); + $xs->element('poco:type', null, 'homepage'); + $xs->element('poco:primary', null, 'true'); + $xs->elementEnd('poco:urls'); + } + + if (isset($this->location)) { + $xs->elementStart('poco:address'); + $xs->element('poco:formatted', null, $this->location); + $xs->elementEnd('poco:address'); + } + + if (isset($this->lat) && isset($this->lon)) { + $this->element( + 'georss:point', + null, + (float)$this->lat . ' ' . (float)$this->lon + ); + } + + // XXX: Should we send all avatar sizes we have? I think + // cliqset does -Z + $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); $xs->element( @@ -829,6 +868,8 @@ class Profile extends Memcached_DataObject $xs->elementEnd('activity:' . $element); + // XXX: Add people tags with plural? + return $xs->getString(); } diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 5e17b20d3..8842840d5 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -109,11 +109,11 @@ class Atom10Feed extends XMLStringer ); } - if (!is_null($uri)) { + if (isset($uri)) { $xs->element('uri', null, $uri); } - if (!is_null(email)) { + if (isset($email)) { $xs->element('email', null, $email); } diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php index f71c721fe..2ad8de455 100644 --- a/lib/atomusernoticefeed.php +++ b/lib/atomusernoticefeed.php @@ -60,8 +60,7 @@ class AtomUserNoticeFeed extends AtomNoticeFeed $this->user = $user; if (!empty($user)) { $profile = $user->getProfile(); - $this->addAuthor($profile->getBestName(), - $user->uri); + $this->addAuthor($profile->nickname, $user->uri); } } -- cgit v1.2.3-54-g00ecf From fae5a15a885b0c108efc4c5e28094f15ffbe8694 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 22 Feb 2010 07:40:20 -0500 Subject: add strongly-suggested link to Profile::asActivityNoun() --- classes/Profile.php | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index 1ba3281ff..faa6367b9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -818,6 +818,10 @@ class Profile extends Memcached_DataObject // title should contain fullname $xs->element('title', null, $this->getBestName()); + $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html'), + $this->profileurl); + // Portable Contacts stuff if (isset($this->bio)) { -- cgit v1.2.3-54-g00ecf From b79d4ed6a1e61c600fdd382f3bdfde62aaa15b3d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 22 Feb 2010 07:43:12 -0500 Subject: add PoCo preferredUsername for nickname in Profile::asActivityNoun() --- classes/Profile.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'classes/Profile.php') diff --git a/classes/Profile.php b/classes/Profile.php index faa6367b9..7fb2b87bc 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -822,6 +822,8 @@ class Profile extends Memcached_DataObject 'type' => 'text/html'), $this->profileurl); + $xs->element('poco:preferredUsername', null, $this->nickname); + // Portable Contacts stuff if (isset($this->bio)) { -- cgit v1.2.3-54-g00ecf From 6a711c6cdc5d1e1b1a64e5858b12e6964a0abe9c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 22 Feb 2010 17:10:50 -0800 Subject: Move ActivityObject and related stuff to core --- classes/Notice.php | 21 +- classes/Profile.php | 78 +--- lib/activity.php | 864 +++++++++++++++++++++++++++++++++++++++ plugins/OStatus/lib/activity.php | 863 -------------------------------------- 4 files changed, 868 insertions(+), 958 deletions(-) create mode 100644 lib/activity.php delete mode 100644 plugins/OStatus/lib/activity.php (limited to 'classes/Profile.php') diff --git a/classes/Notice.php b/classes/Notice.php index a12839d72..ba8646f68 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1119,25 +1119,8 @@ class Notice extends Memcached_DataObject */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - - $xs->elementStart('activity:' . $element); - $xs->element('activity:object-type', - null, - 'http://activitystrea.ms/schema/1.0/note'); - $xs->element('id', - null, - $this->uri); - $xs->element('content', - array('type' => 'text/html'), - $this->rendered); - $xs->element('link', - array('type' => 'text/html', - 'rel' => 'alternate', - 'href' => $this->bestUrl())); - $xs->elementEnd('activity:' . $element); - - return $xs->getString(); + $noun = ActivityObject::fromNotice($this); + return $noun->asString('activity:' . $element); } function bestUrl() diff --git a/classes/Profile.php b/classes/Profile.php index 7fb2b87bc..78223b34a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -801,82 +801,8 @@ class Profile extends Memcached_DataObject */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - - $xs->elementStart('activity:' . $element); - $xs->element( - 'activity:object-type', - null, - 'http://activitystrea.ms/schema/1.0/person' - ); - $xs->element( - 'id', - null, - $this->getUri() - ); - - // title should contain fullname - $xs->element('title', null, $this->getBestName()); - - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html'), - $this->profileurl); - - $xs->element('poco:preferredUsername', null, $this->nickname); - - // Portable Contacts stuff - - if (isset($this->bio)) { - - // XXX: Possible to use OpenSocial's aboutMe? - - $xs->element('poco:note', null, $this->bio); - } - - if (isset($this->homepage)) { - - $xs->elementStart('poco:urls'); - $xs->element('poco:value', null, $this->homepage); - $xs->element('poco:type', null, 'homepage'); - $xs->element('poco:primary', null, 'true'); - $xs->elementEnd('poco:urls'); - } - - if (isset($this->location)) { - $xs->elementStart('poco:address'); - $xs->element('poco:formatted', null, $this->location); - $xs->elementEnd('poco:address'); - } - - if (isset($this->lat) && isset($this->lon)) { - $this->element( - 'georss:point', - null, - (float)$this->lat . ' ' . (float)$this->lon - ); - } - - // XXX: Should we send all avatar sizes we have? I think - // cliqset does -Z - - $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); - - $xs->element( - 'link', array( - 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $avatar->displayUrl() - ), - '' - ); - - $xs->elementEnd('activity:' . $element); - - // XXX: Add people tags with plural? - - return $xs->getString(); + $noun = ActivityObject::fromProfile($this); + return $noun->asString('activity:' . $element); } /** diff --git a/lib/activity.php b/lib/activity.php new file mode 100644 index 000000000..3689dac38 --- /dev/null +++ b/lib/activity.php @@ -0,0 +1,864 @@ +. + * + * @category Feed + * @package StatusNet + * @author Evan Prodromou + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PoCoURL +{ + const TYPE = 'type'; + const VALUE = 'value'; + const PRIMARY = 'primary'; + + public $type; + public $value; + public $primary; + + function __construct($type, $value, $primary = false) + { + $this->type = $type; + $this->value = $value; + $this->primary = $primary; + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->elementStart('poco:urls'); + $xs->element('poco:type', null, $this->type); + $xs->element('poco:value', null, $this->value); + if ($this->primary) { + $xs->element('poco:primary', null, 'true'); + } + $xs->elementEnd('poco:urls'); + return $xs->getString(); + } +} + +class PoCoAddress +{ + const ADDRESS = 'address'; + const FORMATTED = 'formatted'; + + public $formatted; + + function __construct($formatted) + { + if (empty($formatted)) { + return null; + } + $this->formatted = $formatted; + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->elementStart('poco:address'); + $xs->element('poco:formatted', null, $this->formatted); + $xs->elementEnd('poco:address'); + return $xs->getString(); + } +} + +class PoCo +{ + const NS = 'http://portablecontacts.net/spec/1.0'; + + const USERNAME = 'preferredUsername'; + const NOTE = 'note'; + const URLS = 'urls'; + + public $preferredUsername; + public $note; + public $address; + public $urls = array(); + + function __construct($profile) + { + $this->preferredUsername = $profile->nickname; + + $this->note = $profile->bio; + $this->address = new PoCoAddress($profile->location); + + if (!empty($profile->homepage)) { + array_push( + $this->urls, + new PoCoURL( + 'homepage', + $profile->homepage, + true + ) + ); + } + } + + function asString() + { + $xs = new XMLStringer(true); + $xs->element( + 'poco:preferredUsername', + null, + $this->preferredUsername + ); + + if (!empty($this->note)) { + $xs->element('poco:note', null, $this->note); + } + + if (!empty($this->address)) { + $xs->raw($this->address->asString()); + } + + foreach ($this->urls as $url) { + $xs->raw($url->asString()); + } + + return $xs->getString(); + } +} + +/** + * Utilities for turning DOMish things into Activityish things + * + * Some common functions that I didn't have the bandwidth to try to factor + * into some kind of reasonable superclass, so just dumped here. Might + * be useful to have an ActivityObject parent class or something. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityUtils +{ + const ATOM = 'http://www.w3.org/2005/Atom'; + + const LINK = 'link'; + const REL = 'rel'; + const TYPE = 'type'; + const HREF = 'href'; + + const CONTENT = 'content'; + const SRC = 'src'; + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getPermalink($element) + { + return self::getLink($element, 'alternate', 'text/html'); + } + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getLink($element, $rel, $type=null) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + + foreach ($links as $link) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + return $link->getAttribute(self::HREF); + } + } + + return null; + } + + /** + * Gets the first child element with the given tag + * + * @param DOMElement $element element to pick at + * @param string $tag tag to look for + * @param string $namespace Namespace to look under + * + * @return DOMElement found element or null + */ + + static function child($element, $tag, $namespace=self::ATOM) + { + $els = $element->childNodes; + if (empty($els) || $els->length == 0) { + return null; + } else { + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->localName == $tag && $el->namespaceURI == $namespace) { + return $el; + } + } + } + } + + /** + * Grab the text content of a DOM element child of the current element + * + * @param DOMElement $element Element whose children we examine + * @param string $tag Tag to look up + * @param string $namespace Namespace to use, defaults to Atom + * + * @return string content of the child + */ + + static function childContent($element, $tag, $namespace=self::ATOM) + { + $el = self::child($element, $tag, $namespace); + + if (empty($el)) { + return null; + } else { + return $el->textContent; + } + } + + /** + * Get the content of an atom:entry-like object + * + * @param DOMElement $element The element to examine. + * + * @return string unencoded HTML content of the element, like "This -< is HTML." + * + * @todo handle remote content + * @todo handle embedded XML mime types + * @todo handle base64-encoded non-XML and non-text mime types + */ + + static function getContent($element) + { + $contentEl = ActivityUtils::child($element, self::CONTENT); + + if (!empty($contentEl)) { + + $src = $contentEl->getAttribute(self::SRC); + + if (!empty($src)) { + throw new ClientException(_("Can't handle remote content yet.")); + } + + $type = $contentEl->getAttribute(self::TYPE); + + // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3 + + if ($type == 'text') { + return $contentEl->textContent; + } else if ($type == 'html') { + $text = $contentEl->textContent; + return htmlspecialchars_decode($text, ENT_QUOTES); + } else if ($type == 'xhtml') { + $divEl = ActivityUtils::child($contentEl, 'div'); + if (empty($divEl)) { + return null; + } + $doc = $divEl->ownerDocument; + $text = ''; + $children = $divEl->childNodes; + + for ($i = 0; $i < $children->length; $i++) { + $child = $children->item($i); + $text .= $doc->saveXML($child); + } + return trim($text); + } else if (in_array(array('text/xml', 'application/xml'), $type) || + preg_match('#(+|/)xml$#', $type)) { + throw new ClientException(_("Can't handle embedded XML content yet.")); + } else if (strncasecmp($type, 'text/', 5)) { + return $contentEl->textContent; + } else { + throw new ClientException(_("Can't handle embedded Base64 content yet.")); + } + } + } +} + +/** + * A noun-ish thing in the activity universe + * + * The activity streams spec talks about activity objects, while also having + * a tag activity:object, which is in fact an activity object. Aaaaaah! + * + * This is just a thing in the activity universe. Can be the subject, object, + * or indirect object (target!) of an activity verb. Rotten name, and I'm + * propagating it. *sigh* + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityObject +{ + const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; + const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; + const NOTE = 'http://activitystrea.ms/schema/1.0/note'; + const STATUS = 'http://activitystrea.ms/schema/1.0/status'; + const FILE = 'http://activitystrea.ms/schema/1.0/file'; + const PHOTO = 'http://activitystrea.ms/schema/1.0/photo'; + const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album'; + const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist'; + const VIDEO = 'http://activitystrea.ms/schema/1.0/video'; + const AUDIO = 'http://activitystrea.ms/schema/1.0/audio'; + const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark'; + const PERSON = 'http://activitystrea.ms/schema/1.0/person'; + const GROUP = 'http://activitystrea.ms/schema/1.0/group'; + const PLACE = 'http://activitystrea.ms/schema/1.0/place'; + const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; + // ^^^^^^^^^^ tea! + + // Atom elements we snarf + + const TITLE = 'title'; + const SUMMARY = 'summary'; + const ID = 'id'; + const SOURCE = 'source'; + + const NAME = 'name'; + const URI = 'uri'; + const EMAIL = 'email'; + + public $element; + public $type; + public $id; + public $title; + public $summary; + public $content; + public $link; + public $source; + public $avatar; + public $geopoint; + + /** + * Constructor + * + * This probably needs to be refactored + * to generate a local class (ActivityPerson, ActivityFile, ...) + * based on the object type. + * + * @param DOMElement $element DOM thing to turn into an Activity thing + */ + + function __construct($element = null) + { + if (empty($element)) { + return; + } + + $this->element = $element; + + if ($element->tagName == 'author') { + + $this->type = self::PERSON; // XXX: is this fair? + $this->title = $this->_childContent($element, self::NAME); + $this->id = $this->_childContent($element, self::URI); + + if (empty($this->id)) { + $email = $this->_childContent($element, self::EMAIL); + if (!empty($email)) { + // XXX: acct: ? + $this->id = 'mailto:'.$email; + } + } + + } else { + + $this->type = $this->_childContent($element, Activity::OBJECTTYPE, + Activity::SPEC); + + if (empty($this->type)) { + $this->type = ActivityObject::NOTE; + } + + $this->id = $this->_childContent($element, self::ID); + $this->title = $this->_childContent($element, self::TITLE); + $this->summary = $this->_childContent($element, self::SUMMARY); + + $this->source = $this->_getSource($element); + + $this->content = ActivityUtils::getContent($element); + + $this->link = ActivityUtils::getPermalink($element); + + // XXX: grab PoCo stuff + } + + // Some per-type attributes... + if ($this->type == self::PERSON || $this->type == self::GROUP) { + $this->displayName = $this->title; + + // @fixme we may have multiple avatars with different resolutions specified + $this->avatar = ActivityUtils::getLink($element, 'avatar'); + $this->nickname = ActivityUtils::childContent($element, PoCo::USERNAME, PoCo::NS); + } + } + + private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) + { + return ActivityUtils::childContent($element, $tag, $namespace); + } + + // Try to get a unique id for the source feed + + private function _getSource($element) + { + $sourceEl = ActivityUtils::child($element, 'source'); + + if (empty($sourceEl)) { + return null; + } else { + $href = ActivityUtils::getLink($sourceEl, 'self'); + if (!empty($href)) { + return $href; + } else { + return ActivityUtils::childContent($sourceEl, 'id'); + } + } + } + + static function fromNotice($notice) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::NOTE; + + $object->id = $notice->uri; + $object->title = $notice->content; + $object->content = $notice->rendered; + $object->link = $notice->bestUrl(); + + return $object; + } + + static function fromProfile($profile) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::PERSON; + $object->id = $profile->getUri(); + $object->title = $profile->getBestName(); + $object->link = $profile->profileurl; + $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + + if (isset($profile->lat) && isset($profile->lon)) { + $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; + } + + $object->poco = new PoCo($profile); + + return $object; + } + + function asString($tag='activity:object') + { + $xs = new XMLStringer(true); + + $xs->elementStart($tag); + + $xs->element('activity:object-type', null, $this->type); + + $xs->element(self::ID, null, $this->id); + + if (!empty($this->title)) { + $xs->element(self::TITLE, null, $this->title); + } + + if (!empty($this->summary)) { + $xs->element(self::SUMMARY, null, $this->summary); + } + + if (!empty($this->content)) { + // XXX: assuming HTML content here + $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content); + } + + if (!empty($this->link)) { + $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), + $this->link); + } + + if ($this->type == ActivityObject::PERSON) { + $xs->element( + 'link', array( + 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, + 'rel' => 'avatar', + 'href' => empty($this->avatar) + ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) + : $this->avatar->displayUrl() + ), + '' + ); + } + + if (!empty($this->geopoint)) { + $xs->element( + 'georss:point', + null, + $this->geopoint + ); + } + + if (!empty($this->poco)) { + $xs->raw($this->poco->asString()); + } + + $xs->elementEnd($tag); + + return $xs->getString(); + } +} + +/** + * Utility class to hold a bunch of constant defining default verb types + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityVerb +{ + const POST = 'http://activitystrea.ms/schema/1.0/post'; + const SHARE = 'http://activitystrea.ms/schema/1.0/share'; + const SAVE = 'http://activitystrea.ms/schema/1.0/save'; + const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite'; + const PLAY = 'http://activitystrea.ms/schema/1.0/play'; + const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow'; + const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; + const JOIN = 'http://activitystrea.ms/schema/1.0/join'; + const TAG = 'http://activitystrea.ms/schema/1.0/tag'; + + // Custom OStatus verbs for the flipside until they're standardized + const DELETE = 'http://ostatus.org/schema/1.0/unfollow'; + const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite'; + const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow'; + const LEAVE = 'http://ostatus.org/schema/1.0/leave'; +} + +class ActivityContext +{ + public $replyToID; + public $replyToUrl; + public $location; + public $attention = array(); + public $conversation; + + const THR = 'http://purl.org/syndication/thread/1.0'; + const GEORSS = 'http://www.georss.org/georss'; + const OSTATUS = 'http://ostatus.org/schema/1.0'; + + const INREPLYTO = 'in-reply-to'; + const REF = 'ref'; + const HREF = 'href'; + + const POINT = 'point'; + + const ATTENTION = 'ostatus:attention'; + const CONVERSATION = 'ostatus:conversation'; + + function __construct($element) + { + $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR); + + if (!empty($replyToEl)) { + $this->replyToID = $replyToEl->getAttribute(self::REF); + $this->replyToUrl = $replyToEl->getAttribute(self::HREF); + } + + $this->location = $this->getLocation($element); + + $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION); + + // Multiple attention links allowed + + $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK); + + for ($i = 0; $i < $links->length; $i++) { + + $link = $links->item($i); + + $linkRel = $link->getAttribute(ActivityUtils::REL); + + if ($linkRel == self::ATTENTION) { + $this->attention[] = $link->getAttribute(self::HREF); + } + } + } + + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($dom) + { + $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT); + + for ($i = 0; $i < $points->length; $i++) { + $point = $points->item($i)->textContent; + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); + $coords = explode(' ', $point); + if (count($coords) == 2) { + list($lat, $lon) = $coords; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + } + + return null; + } +} + +/** + * An activity in the ActivityStrea.ms world + * + * An activity is kind of like a sentence: someone did something + * to something else. + * + * 'someone' is the 'actor'; 'did something' is the verb; + * 'something else' is the object. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class Activity +{ + const SPEC = 'http://activitystrea.ms/spec/1.0/'; + const SCHEMA = 'http://activitystrea.ms/schema/1.0/'; + + const VERB = 'verb'; + const OBJECT = 'object'; + const ACTOR = 'actor'; + const SUBJECT = 'subject'; + const OBJECTTYPE = 'object-type'; + const CONTEXT = 'context'; + const TARGET = 'target'; + + const ATOM = 'http://www.w3.org/2005/Atom'; + + const AUTHOR = 'author'; + const PUBLISHED = 'published'; + const UPDATED = 'updated'; + + public $actor; // an ActivityObject + public $verb; // a string (the URL) + public $object; // an ActivityObject + public $target; // an ActivityObject + public $context; // an ActivityObject + public $time; // Time of the activity + public $link; // an ActivityObject + public $entry; // the source entry + public $feed; // the source feed + + public $summary; // summary of activity + public $content; // HTML content of activity + public $id; // ID of the activity + public $title; // title of the activity + + /** + * Turns a regular old Atom into a magical activity + * + * @param DOMElement $entry Atom entry to poke at + * @param DOMElement $feed Atom feed, for context + */ + + function __construct($entry = null, $feed = null) + { + if (is_null($entry)) { + return; + } + + $this->entry = $entry; + $this->feed = $feed; + + $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); + + if (!empty($pubEl)) { + $this->time = strtotime($pubEl->textContent); + } else { + // XXX technically an error; being liberal. Good idea...? + $updateEl = $this->_child($entry, self::UPDATED, self::ATOM); + if (!empty($updateEl)) { + $this->time = strtotime($updateEl->textContent); + } else { + $this->time = null; + } + } + + $this->link = ActivityUtils::getPermalink($entry); + + $verbEl = $this->_child($entry, self::VERB); + + if (!empty($verbEl)) { + $this->verb = trim($verbEl->textContent); + } else { + $this->verb = ActivityVerb::POST; + // XXX: do other implied stuff here + } + + $objectEl = $this->_child($entry, self::OBJECT); + + if (!empty($objectEl)) { + $this->object = new ActivityObject($objectEl); + } else { + $this->object = new ActivityObject($entry); + } + + $actorEl = $this->_child($entry, self::ACTOR); + + if (!empty($actorEl)) { + + $this->actor = new ActivityObject($actorEl); + + } else if (!empty($feed) && + $subjectEl = $this->_child($feed, self::SUBJECT)) { + + $this->actor = new ActivityObject($subjectEl); + + } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); + + } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, + self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); + } + + $contextEl = $this->_child($entry, self::CONTEXT); + + if (!empty($contextEl)) { + $this->context = new ActivityContext($contextEl); + } else { + $this->context = new ActivityContext($entry); + } + + $targetEl = $this->_child($entry, self::TARGET); + + if (!empty($targetEl)) { + $this->target = new ActivityObject($targetEl); + } + + $this->summary = ActivityUtils::childContent($entry, 'summary'); + $this->id = ActivityUtils::childContent($entry, 'id'); + $this->content = ActivityUtils::getContent($entry); + } + + /** + * Returns an Atom based on this activity + * + * @return DOMElement Atom entry + */ + + function toAtomEntry() + { + return null; + } + + function asString($namespace=false) + { + $xs = new XMLStringer(true); + + if ($namespace) { + $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); + } else { + $attrs = array(); + } + + $xs->elementStart('entry', $attrs); + + $xs->element('id', null, $this->id); + $xs->element('title', null, $this->title); + $xs->element('published', null, common_date_iso8601($this->time)); + $xs->element('content', array('type' => 'html'), $this->content); + + if (!empty($this->summary)) { + $xs->element('summary', null, $this->summary); + } + + if (!empty($this->link)) { + $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html'), + $this->link); + } + + // XXX: add context + // XXX: add target + + $xs->raw($this->actor->asString('activity:actor')); + $xs->element('activity:verb', null, $this->verb); + $xs->raw($this->object->asString()); + + $xs->elementEnd('entry'); + + return $xs->getString(); + } + + private function _child($element, $tag, $namespace=self::SPEC) + { + return ActivityUtils::child($element, $tag, $namespace); + } +} \ No newline at end of file diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php deleted file mode 100644 index 585028318..000000000 --- a/plugins/OStatus/lib/activity.php +++ /dev/null @@ -1,863 +0,0 @@ -. - * - * @category OStatus - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class PoCoURL -{ - const TYPE = 'type'; - const VALUE = 'value'; - const PRIMARY = 'primary'; - - public $type; - public $value; - public $primary; - - function __construct($type, $value, $primary = false) - { - $this->type = $type; - $this->value = $value; - $this->primary = $primary; - } - - function asString() - { - $xs = new XMLStringer(true); - $xs->elementStart('poco:urls'); - $xs->element('poco:type', null, $this->type); - $xs->element('poco:value', null, $this->value); - if ($this->primary) { - $xs->element('poco:primary', null, 'true'); - } - $xs->elementEnd('poco:urls'); - return $xs->getString(); - } -} - -class PoCoAddress -{ - const ADDRESS = 'address'; - const FORMATTED = 'formatted'; - - public $formatted; - - function __construct($formatted) - { - if (empty($formatted)) { - return null; - } - $this->formatted = $formatted; - } - - function asString() - { - $xs = new XMLStringer(true); - $xs->elementStart('poco:address'); - $xs->element('poco:formatted', null, $this->formatted); - $xs->elementEnd('poco:address'); - return $xs->getString(); - } -} - -class PoCo -{ - const NS = 'http://portablecontacts.net/spec/1.0'; - - const USERNAME = 'preferredUsername'; - const NOTE = 'note'; - const URLS = 'urls'; - - public $preferredUsername; - public $note; - public $address; - public $urls = array(); - - function __construct($profile) - { - $this->preferredUsername = $profile->nickname; - - $this->note = $profile->bio; - $this->address = new PoCoAddress($profile->location); - - if (!empty($profile->homepage)) { - array_push( - $this->urls, - new PoCoURL( - 'homepage', - $profile->homepage, - true - ) - ); - } - } - - function asString() - { - $xs = new XMLStringer(true); - $xs->element( - 'poco:preferredUsername', - null, - $this->preferredUsername - ); - - if (!empty($this->note)) { - $xs->element('poco:note', null, $this->note); - } - - if (!empty($this->address)) { - $xs->raw($this->address->asString()); - } - - foreach ($this->urls as $url) { - $xs->raw($url->asString()); - } - - return $xs->getString(); - } -} - -/** - * Utilities for turning DOMish things into Activityish things - * - * Some common functions that I didn't have the bandwidth to try to factor - * into some kind of reasonable superclass, so just dumped here. Might - * be useful to have an ActivityObject parent class or something. - * - * @category OStatus - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 - * @link http://status.net/ - */ - -class ActivityUtils -{ - const ATOM = 'http://www.w3.org/2005/Atom'; - - const LINK = 'link'; - const REL = 'rel'; - const TYPE = 'type'; - const HREF = 'href'; - - const CONTENT = 'content'; - const SRC = 'src'; - - /** - * Get the permalink for an Activity object - * - * @param DOMElement $element A DOM element - * - * @return string related link, if any - */ - - static function getPermalink($element) - { - return self::getLink($element, 'alternate', 'text/html'); - } - - /** - * Get the permalink for an Activity object - * - * @param DOMElement $element A DOM element - * - * @return string related link, if any - */ - - static function getLink($element, $rel, $type=null) - { - $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); - - foreach ($links as $link) { - - $linkRel = $link->getAttribute(self::REL); - $linkType = $link->getAttribute(self::TYPE); - - if ($linkRel == $rel && - (is_null($type) || $linkType == $type)) { - return $link->getAttribute(self::HREF); - } - } - - return null; - } - - /** - * Gets the first child element with the given tag - * - * @param DOMElement $element element to pick at - * @param string $tag tag to look for - * @param string $namespace Namespace to look under - * - * @return DOMElement found element or null - */ - - static function child($element, $tag, $namespace=self::ATOM) - { - $els = $element->childNodes; - if (empty($els) || $els->length == 0) { - return null; - } else { - for ($i = 0; $i < $els->length; $i++) { - $el = $els->item($i); - if ($el->localName == $tag && $el->namespaceURI == $namespace) { - return $el; - } - } - } - } - - /** - * Grab the text content of a DOM element child of the current element - * - * @param DOMElement $element Element whose children we examine - * @param string $tag Tag to look up - * @param string $namespace Namespace to use, defaults to Atom - * - * @return string content of the child - */ - - static function childContent($element, $tag, $namespace=self::ATOM) - { - $el = self::child($element, $tag, $namespace); - - if (empty($el)) { - return null; - } else { - return $el->textContent; - } - } - - /** - * Get the content of an atom:entry-like object - * - * @param DOMElement $element The element to examine. - * - * @return string unencoded HTML content of the element, like "This -< is HTML." - * - * @todo handle remote content - * @todo handle embedded XML mime types - * @todo handle base64-encoded non-XML and non-text mime types - */ - - static function getContent($element) - { - $contentEl = ActivityUtils::child($element, self::CONTENT); - - if (!empty($contentEl)) { - - $src = $contentEl->getAttribute(self::SRC); - - if (!empty($src)) { - throw new ClientException(_("Can't handle remote content yet.")); - } - - $type = $contentEl->getAttribute(self::TYPE); - - // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3 - - if ($type == 'text') { - return $contentEl->textContent; - } else if ($type == 'html') { - $text = $contentEl->textContent; - return htmlspecialchars_decode($text, ENT_QUOTES); - } else if ($type == 'xhtml') { - $divEl = ActivityUtils::child($contentEl, 'div'); - if (empty($divEl)) { - return null; - } - $doc = $divEl->ownerDocument; - $text = ''; - $children = $divEl->childNodes; - - for ($i = 0; $i < $children->length; $i++) { - $child = $children->item($i); - $text .= $doc->saveXML($child); - } - return trim($text); - } else if (in_array(array('text/xml', 'application/xml'), $type) || - preg_match('#(+|/)xml$#', $type)) { - throw new ClientException(_("Can't handle embedded XML content yet.")); - } else if (strncasecmp($type, 'text/', 5)) { - return $contentEl->textContent; - } else { - throw new ClientException(_("Can't handle embedded Base64 content yet.")); - } - } - } -} - -/** - * A noun-ish thing in the activity universe - * - * The activity streams spec talks about activity objects, while also having - * a tag activity:object, which is in fact an activity object. Aaaaaah! - * - * This is just a thing in the activity universe. Can be the subject, object, - * or indirect object (target!) of an activity verb. Rotten name, and I'm - * propagating it. *sigh* - * - * @category OStatus - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 - * @link http://status.net/ - */ - -class ActivityObject -{ - const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; - const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; - const NOTE = 'http://activitystrea.ms/schema/1.0/note'; - const STATUS = 'http://activitystrea.ms/schema/1.0/status'; - const FILE = 'http://activitystrea.ms/schema/1.0/file'; - const PHOTO = 'http://activitystrea.ms/schema/1.0/photo'; - const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album'; - const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist'; - const VIDEO = 'http://activitystrea.ms/schema/1.0/video'; - const AUDIO = 'http://activitystrea.ms/schema/1.0/audio'; - const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark'; - const PERSON = 'http://activitystrea.ms/schema/1.0/person'; - const GROUP = 'http://activitystrea.ms/schema/1.0/group'; - const PLACE = 'http://activitystrea.ms/schema/1.0/place'; - const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; - // ^^^^^^^^^^ tea! - - // Atom elements we snarf - - const TITLE = 'title'; - const SUMMARY = 'summary'; - const ID = 'id'; - const SOURCE = 'source'; - - const NAME = 'name'; - const URI = 'uri'; - const EMAIL = 'email'; - - public $element; - public $type; - public $id; - public $title; - public $summary; - public $content; - public $link; - public $source; - public $avatar; - public $geopoint; - - /** - * Constructor - * - * This probably needs to be refactored - * to generate a local class (ActivityPerson, ActivityFile, ...) - * based on the object type. - * - * @param DOMElement $element DOM thing to turn into an Activity thing - */ - - function __construct($element = null) - { - if (empty($element)) { - return; - } - - $this->element = $element; - - if ($element->tagName == 'author') { - - $this->type = self::PERSON; // XXX: is this fair? - $this->title = $this->_childContent($element, self::NAME); - $this->id = $this->_childContent($element, self::URI); - - if (empty($this->id)) { - $email = $this->_childContent($element, self::EMAIL); - if (!empty($email)) { - // XXX: acct: ? - $this->id = 'mailto:'.$email; - } - } - - } else { - - $this->type = $this->_childContent($element, Activity::OBJECTTYPE, - Activity::SPEC); - - if (empty($this->type)) { - $this->type = ActivityObject::NOTE; - } - - $this->id = $this->_childContent($element, self::ID); - $this->title = $this->_childContent($element, self::TITLE); - $this->summary = $this->_childContent($element, self::SUMMARY); - - $this->source = $this->_getSource($element); - - $this->content = ActivityUtils::getContent($element); - - $this->link = ActivityUtils::getPermalink($element); - - // XXX: grab PoCo stuff - } - - // Some per-type attributes... - if ($this->type == self::PERSON || $this->type == self::GROUP) { - $this->displayName = $this->title; - - // @fixme we may have multiple avatars with different resolutions specified - $this->avatar = ActivityUtils::getLink($element, 'avatar'); - $this->nickname = ActivityUtils::childContent($element, PoCo::USERNAME, PoCo::NS); - } - } - - private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) - { - return ActivityUtils::childContent($element, $tag, $namespace); - } - - // Try to get a unique id for the source feed - - private function _getSource($element) - { - $sourceEl = ActivityUtils::child($element, 'source'); - - if (empty($sourceEl)) { - return null; - } else { - $href = ActivityUtils::getLink($sourceEl, 'self'); - if (!empty($href)) { - return $href; - } else { - return ActivityUtils::childContent($sourceEl, 'id'); - } - } - } - - static function fromNotice($notice) - { - $object = new ActivityObject(); - - $object->type = ActivityObject::NOTE; - - $object->id = $notice->uri; - $object->title = $notice->content; - $object->content = $notice->rendered; - $object->link = $notice->bestUrl(); - - return $object; - } - - static function fromProfile($profile) - { - $object = new ActivityObject(); - - $object->type = ActivityObject::PERSON; - $object->id = $profile->getUri(); - $object->title = $profile->getBestName(); - $object->link = $profile->profileurl; - $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - - if (isset($profile->lat) && isset($profile->lon)) { - $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; - } - - $object->poco = new PoCo($profile); - - return $object; - } - - function asString($tag='activity:object') - { - $xs = new XMLStringer(true); - - $xs->elementStart($tag); - - $xs->element('activity:object-type', null, $this->type); - - $xs->element(self::ID, null, $this->id); - - if (!empty($this->title)) { - $xs->element(self::TITLE, null, $this->title); - } - - if (!empty($this->summary)) { - $xs->element(self::SUMMARY, null, $this->summary); - } - - if (!empty($this->content)) { - // XXX: assuming HTML content here - $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content); - } - - if (!empty($this->link)) { - $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), - $this->link); - } - - if ($this->type == ActivityObject::PERSON) { - $xs->element( - 'link', array( - 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($this->avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $this->avatar->displayUrl() - ), - '' - ); - } - - if (!empty($this->geopoint)) { - $xs->element( - 'georss:point', - null, - $this->geopoint - ); - } - - if (!empty($this->poco)) { - $xs->raw($this->poco->asString()); - } - - $xs->elementEnd($tag); - - return $xs->getString(); - } -} - -/** - * Utility class to hold a bunch of constant defining default verb types - * - * @category OStatus - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 - * @link http://status.net/ - */ - -class ActivityVerb -{ - const POST = 'http://activitystrea.ms/schema/1.0/post'; - const SHARE = 'http://activitystrea.ms/schema/1.0/share'; - const SAVE = 'http://activitystrea.ms/schema/1.0/save'; - const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite'; - const PLAY = 'http://activitystrea.ms/schema/1.0/play'; - const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow'; - const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; - const JOIN = 'http://activitystrea.ms/schema/1.0/join'; - const TAG = 'http://activitystrea.ms/schema/1.0/tag'; - - // Custom OStatus verbs for the flipside until they're standardized - const DELETE = 'http://ostatus.org/schema/1.0/unfollow'; - const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite'; - const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow'; - const LEAVE = 'http://ostatus.org/schema/1.0/leave'; -} - -class ActivityContext -{ - public $replyToID; - public $replyToUrl; - public $location; - public $attention = array(); - public $conversation; - - const THR = 'http://purl.org/syndication/thread/1.0'; - const GEORSS = 'http://www.georss.org/georss'; - const OSTATUS = 'http://ostatus.org/schema/1.0'; - - const INREPLYTO = 'in-reply-to'; - const REF = 'ref'; - const HREF = 'href'; - - const POINT = 'point'; - - const ATTENTION = 'ostatus:attention'; - const CONVERSATION = 'ostatus:conversation'; - - function __construct($element) - { - $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR); - - if (!empty($replyToEl)) { - $this->replyToID = $replyToEl->getAttribute(self::REF); - $this->replyToUrl = $replyToEl->getAttribute(self::HREF); - } - - $this->location = $this->getLocation($element); - - $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION); - - // Multiple attention links allowed - - $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK); - - for ($i = 0; $i < $links->length; $i++) { - - $link = $links->item($i); - - $linkRel = $link->getAttribute(ActivityUtils::REL); - - if ($linkRel == self::ATTENTION) { - $this->attention[] = $link->getAttribute(self::HREF); - } - } - } - - /** - * Parse location given as a GeoRSS-simple point, if provided. - * http://www.georss.org/simple - * - * @param feed item $entry - * @return mixed Location or false - */ - function getLocation($dom) - { - $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT); - - for ($i = 0; $i < $points->length; $i++) { - $point = $points->item($i)->textContent; - $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" - $point = preg_replace('/\s+/', ' ', $point); - $point = trim($point); - $coords = explode(' ', $point); - if (count($coords) == 2) { - list($lat, $lon) = $coords; - if (is_numeric($lat) && is_numeric($lon)) { - common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); - return Location::fromLatLon($lat, $lon); - } - } - common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); - } - - return null; - } -} - -/** - * An activity in the ActivityStrea.ms world - * - * An activity is kind of like a sentence: someone did something - * to something else. - * - * 'someone' is the 'actor'; 'did something' is the verb; - * 'something else' is the object. - * - * @category OStatus - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 - * @link http://status.net/ - */ - -class Activity -{ - const SPEC = 'http://activitystrea.ms/spec/1.0/'; - const SCHEMA = 'http://activitystrea.ms/schema/1.0/'; - - const VERB = 'verb'; - const OBJECT = 'object'; - const ACTOR = 'actor'; - const SUBJECT = 'subject'; - const OBJECTTYPE = 'object-type'; - const CONTEXT = 'context'; - const TARGET = 'target'; - - const ATOM = 'http://www.w3.org/2005/Atom'; - - const AUTHOR = 'author'; - const PUBLISHED = 'published'; - const UPDATED = 'updated'; - - public $actor; // an ActivityObject - public $verb; // a string (the URL) - public $object; // an ActivityObject - public $target; // an ActivityObject - public $context; // an ActivityObject - public $time; // Time of the activity - public $link; // an ActivityObject - public $entry; // the source entry - public $feed; // the source feed - - public $summary; // summary of activity - public $content; // HTML content of activity - public $id; // ID of the activity - public $title; // title of the activity - - /** - * Turns a regular old Atom into a magical activity - * - * @param DOMElement $entry Atom entry to poke at - * @param DOMElement $feed Atom feed, for context - */ - - function __construct($entry = null, $feed = null) - { - if (is_null($entry)) { - return; - } - - $this->entry = $entry; - $this->feed = $feed; - - $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); - - if (!empty($pubEl)) { - $this->time = strtotime($pubEl->textContent); - } else { - // XXX technically an error; being liberal. Good idea...? - $updateEl = $this->_child($entry, self::UPDATED, self::ATOM); - if (!empty($updateEl)) { - $this->time = strtotime($updateEl->textContent); - } else { - $this->time = null; - } - } - - $this->link = ActivityUtils::getPermalink($entry); - - $verbEl = $this->_child($entry, self::VERB); - - if (!empty($verbEl)) { - $this->verb = trim($verbEl->textContent); - } else { - $this->verb = ActivityVerb::POST; - // XXX: do other implied stuff here - } - - $objectEl = $this->_child($entry, self::OBJECT); - - if (!empty($objectEl)) { - $this->object = new ActivityObject($objectEl); - } else { - $this->object = new ActivityObject($entry); - } - - $actorEl = $this->_child($entry, self::ACTOR); - - if (!empty($actorEl)) { - - $this->actor = new ActivityObject($actorEl); - - } else if (!empty($feed) && - $subjectEl = $this->_child($feed, self::SUBJECT)) { - - $this->actor = new ActivityObject($subjectEl); - - } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { - - $this->actor = new ActivityObject($authorEl); - - } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, - self::ATOM)) { - - $this->actor = new ActivityObject($authorEl); - } - - $contextEl = $this->_child($entry, self::CONTEXT); - - if (!empty($contextEl)) { - $this->context = new ActivityContext($contextEl); - } else { - $this->context = new ActivityContext($entry); - } - - $targetEl = $this->_child($entry, self::TARGET); - - if (!empty($targetEl)) { - $this->target = new ActivityObject($targetEl); - } - - $this->summary = ActivityUtils::childContent($entry, 'summary'); - $this->id = ActivityUtils::childContent($entry, 'id'); - $this->content = ActivityUtils::getContent($entry); - } - - /** - * Returns an Atom based on this activity - * - * @return DOMElement Atom entry - */ - - function toAtomEntry() - { - return null; - } - - function asString($namespace=false) - { - $xs = new XMLStringer(true); - - if ($namespace) { - $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); - } else { - $attrs = array(); - } - - $xs->elementStart('entry', $attrs); - - $xs->element('id', null, $this->id); - $xs->element('title', null, $this->title); - $xs->element('published', null, common_date_iso8601($this->time)); - $xs->element('content', array('type' => 'html'), $this->content); - - if (!empty($this->summary)) { - $xs->element('summary', null, $this->summary); - } - - if (!empty($this->link)) { - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html'), - $this->link); - } - - // XXX: add context - // XXX: add target - - $xs->raw($this->actor->asString('activity:actor')); - $xs->element('activity:verb', null, $this->verb); - $xs->raw($this->object->asString()); - - $xs->elementEnd('entry'); - - return $xs->getString(); - } - - private function _child($element, $tag, $namespace=self::SPEC) - { - return ActivityUtils::child($element, $tag, $namespace); - } -} \ No newline at end of file -- cgit v1.2.3-54-g00ecf