From a1b436a8c6a733dc0c11b3b9421b4db613a62fd1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 15 Feb 2011 20:25:39 -0800 Subject: [PATCH 01/16] First cut at some JSON Activity Streams output --- actions/apitimelinefriends.php | 6 ++ classes/Notice.php | 5 +- lib/activity.php | 53 ++++++++++++++ lib/activityobject.php | 48 ++++++++++++- lib/activitystreamjsondocument.php | 112 +++++++++++++++++++++++++++++ lib/router.php | 2 +- 6 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 lib/activitystreamjsondocument.php diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 71049f6eb1..3833418baa 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -263,6 +263,12 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/classes/Notice.php b/classes/Notice.php index 4522d1fc38..ed1aab0014 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1252,18 +1252,19 @@ class Notice extends Memcached_DataObject function asActivity() { + common_debug("a"); + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); if (!empty($act)) { return $act; } - $act = new Activity(); if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { $profile = $this->getProfile(); - + common_debug('b'); $act->actor = ActivityObject::fromProfile($profile); $act->verb = ActivityVerb::POST; $act->objects[] = ActivityObject::fromNotice($this); diff --git a/lib/activity.php b/lib/activity.php index 17684d897b..0046469899 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -337,6 +337,59 @@ class Activity return null; } + /** + * Returns an array based on this activity suitable + * for encoding as a JSON object + * + * @return array $activity + */ + + function asArray() + { + $activity = array(); + + // actor + $activity['actor'] = $this->actor->asArray(); + + // body + $activity['body'] = $this->content; + + // generator <--- might be useful; might be too much junk + + // icon <-- should we use this? + + // object + if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { + $activity['object'] = $this->objects[0]->asArray(); + } else { + $activity['object'] = array(); + foreach($this->objects as $object) { + $activity['object'][] = $object->asArray(); + } + } + + $activity['postedTime'] = self::iso8601Date($this->time); // Change to exactly be RFC3339? + + // provider <--- again not sure we should use this + + // target + if (!empty($this->target)) { + $activity['target'] = $this->target->asArray(); + } + + // title + $activity['title'] = $this->title; + + // updatedTime <-- should we use? spec says activity MAY have this + + // verb + $activity['verb'] = $this->verb; + + // TODO: extensions (ActivityContext, OStatus stuff, etc.) + + return $activity; + } + function asString($namespace=false, $author=true, $source=false) { $xs = new XMLStringer(true); diff --git a/lib/activityobject.php b/lib/activityobject.php index 5898c6d050..53ffe1a172 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -179,7 +179,7 @@ class ActivityObject if (empty($this->type)) { $this->type = self::PERSON; // XXX: is this fair? } - + // start with $title = ActivityUtils::childHtmlContent($element, self::TITLE); @@ -419,7 +419,7 @@ class ActivityObject static function fromNotice(Notice $notice) { $object = new ActivityObject(); - + if (Event::handle('StartActivityObjectFromNotice', array($notice, &$object))) { $object->type = ActivityObject::NOTE; @@ -526,7 +526,7 @@ class ActivityObject return $object; } - + function outputTo($xo, $tag='activity:object') { if (!empty($tag)) { @@ -633,4 +633,46 @@ class ActivityObject return $xs->getString(); } + + /* + * Returns an array based on this Activity Object suitable for + * encoding as JSON. + * + * @return array $object the activity object array + */ + + function asArray() + { + $object = array(); + + // TODO: attachedObjects + + // displayName + $object['displayName'] = $this->title; + + + // TODO: downstreamDuplicates + // TODO: embedCode + + // id + $object['id'] = $this->id; + + // TODO: image + // Need to make MediaLink serialization + + // objectType + $object['type'] = $this->type; + + // summary + $object['summary'] = $this->summary; + + // TODO: upstreamDuplicates + + // url (XXX: need to put the right thing here...) + $object['url'] = $this->id; + + // TODO: extensions (OStatus stuff, etc.) + + return $object; + } } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php new file mode 100644 index 0000000000..57ece9f6bf --- /dev/null +++ b/lib/activitystreamjsondocument.php @@ -0,0 +1,112 @@ +. + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) +{ + exit(1); +} + +/** + * A class for generating JSON documents that represent an Activity Streams + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ActivityStreamJSONDocument +{ + /* Top level array representing the document */ + protected $doc = array(); + + /* The current authenticated user */ + protected $cur = null; + + /** + * Constructor + * + * @param User $cur the current authenticated user + */ + + function __construct($cur = null) + { + $this->cur = $cur; + + $this->doc['items'] = array(); + } + + /** + * Add more than one Item to the document + * + * @param mixed $notices an array of Notice objects or handle + * + */ + + function addItemsFromNotices($notices) + { + common_debug("addItemsFromNotices"); + if (is_array($notices)) { + foreach ($notices as $notice) { + $this->addItemFromNotice($notice); + } + } else { + while ($notices->fetch()) { + $this->addItemFromNotice($notices); + } + } + } + + /** + * Add a single Notice to the document + * + * @param Notice $notice a Notice to add + */ + + function addItemFromNotice($notice) + { + $cur = empty($this->cur) ? common_current_user() : $this->cur; + + $act = $notice->asActivity(); + $act->extra[] = $notice->noticeInfo($cur); + + array_push($this->doc['items'], $act->asArray()); + } + + /* + * Return the entire document as a big string of JSON + * + * @return string encoded JSON output + */ + function asString() + { + return json_encode($this->doc); + } + +} diff --git a/lib/router.php b/lib/router.php index c8e1c365a5..9f2b1df868 100644 --- a/lib/router.php +++ b/lib/router.php @@ -411,7 +411,7 @@ class Router $m->connect('api/statuses/friends_timeline.:format', array('action' => 'ApiTimelineFriends', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/friends_timeline/:id.:format', array('action' => 'ApiTimelineFriends', From 764a29e2ff0010eda99a6ced14172045b41751bf Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Feb 2011 16:21:31 -0800 Subject: [PATCH 02/16] Remove debugging statements I accidentally left in --- classes/Notice.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index ed1aab0014..007eb55f85 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1252,8 +1252,6 @@ class Notice extends Memcached_DataObject function asActivity() { - common_debug("a"); - $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); if (!empty($act)) { @@ -1264,7 +1262,6 @@ class Notice extends Memcached_DataObject if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { $profile = $this->getProfile(); - common_debug('b'); $act->actor = ActivityObject::fromProfile($profile); $act->verb = ActivityVerb::POST; $act->objects[] = ActivityObject::fromNotice($this); From e4d5c47ebf60adcc08a4ca48dd6e398c3c419993 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Feb 2011 16:21:46 -0800 Subject: [PATCH 03/16] Add image to JSON ActivityObject and title + links to the JSON document --- actions/apitimelinefriends.php | 2 + lib/activity.php | 2 +- lib/activityobject.php | 30 ++++++-- lib/activitystreamjsondocument.php | 108 ++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 6 deletions(-) diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 3833418baa..0e356bb18b 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -266,6 +266,8 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction case 'as': header('Content-Type: application/json; charset=utf-8'); $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link,'alternate', 'text/html'); $doc->addItemsFromNotices($this->notices); $this->raw($doc->asString()); break; diff --git a/lib/activity.php b/lib/activity.php index 0046469899..e6fefca28f 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -387,7 +387,7 @@ class Activity // TODO: extensions (ActivityContext, OStatus stuff, etc.) - return $activity; + return array_filter($activity); } function asString($namespace=false, $author=true, $source=false) diff --git a/lib/activityobject.php b/lib/activityobject.php index 53ffe1a172..17753f0df4 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -652,13 +652,35 @@ class ActivityObject // TODO: downstreamDuplicates - // TODO: embedCode + // TODO: embedCode (video) // id $object['id'] = $this->id; - // TODO: image - // Need to make MediaLink serialization + if ($this->type == ActivityObject::PERSON + || $this->type == ActivityObject::GROUP) { + + // XXX: Not sure what the best avatar is to use for the + // author's "image". For now, I'm using the stream size + // one, but possibly it should be large + $avatarLink = null; + + foreach ($this->avatarLinks as $a) { + if ($a->height == AVATAR_STREAM_SIZE) { + $avatarLink = $a; + break; + } + } + + $imgLink = new ActivityStreamsMediaLink( + $avatarLink->url, + $avatarLink->width, + $avatarLink->height, + $avatarLink->type + ); + + $object['image'] = $imgLink->asArray(); + } // objectType $object['type'] = $this->type; @@ -673,6 +695,6 @@ class ActivityObject // TODO: extensions (OStatus stuff, etc.) - return $object; + return array_filter($object); } } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index 57ece9f6bf..4fa456470e 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -43,6 +43,7 @@ if (!defined('STATUSNET')) */ class ActivityStreamJSONDocument { + /* Top level array representing the document */ protected $doc = array(); @@ -57,9 +58,28 @@ class ActivityStreamJSONDocument function __construct($cur = null) { + $this->cur = $cur; + /* Title of the JSON document */ + $this->doc['title'] = null; + + /* Array of activity items */ $this->doc['items'] = array(); + + /* Array of links associated with the document */ + $this->doc['links'] = array(); + + } + + /** + * Set the title of the document + * + * @param String $title the title + */ + function setTitle($title) + { + $this->doc['title'] = $title; } /** @@ -99,6 +119,18 @@ class ActivityStreamJSONDocument array_push($this->doc['items'], $act->asArray()); } + /** + * Add a link to the JSON document + * + * @param string $url the URL for the link + * @param string $rel the link relationship + */ + function addLink($url = null, $rel = null, $mediaType = null) + { + $link = new ActivityStreamsLink($url, $rel, $mediaType); + $this->doc['link'][] = $link->asArray(); + } + /* * Return the entire document as a big string of JSON * @@ -106,7 +138,81 @@ class ActivityStreamJSONDocument */ function asString() { - return json_encode($this->doc); + return json_encode(array_filter($this->doc)); } } + +/** + * A class for representing MediaLinks in JSON Activities + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ActivityStreamsMediaLink extends ActivityStreamsLink +{ + private $linkDict; + + function __construct( + $url = null, + $width = null, + $height = null, + $mediaType = null, + $rel = null, + $duration = null + ) + { + parent::__construct($url, $rel, $mediaType); + $this->linkDict = array( + 'width' => $width, + 'height' => $height, + 'duration' => $duration + ); + } + + function asArray() + { + return array_merge( + parent::asArray(), + array_filter($this->linkDict) + ); + } +} + +/** + * A class for representing links in JSON Activities + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ActivityStreamsLink +{ + private $linkDict; + + function __construct($url = null, $rel = null, $mediaType = null) + { + // links MUST have a URL + if (empty($url)) { + throw new Exception('Links must have a URL.'); + } + + $this->linkDict = array( + 'url' => $url, + 'rel' => $rel, // extension + 'media_type' => $mediaType // extension + ); + } + + function asArray() + { + return array_filter($this->linkDict); + } +} From ddda31038e0db5836f22cf22b61e4229b556c91e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Feb 2011 16:44:02 -0800 Subject: [PATCH 04/16] Use simple relative object and verb references --- lib/activity.php | 5 ++++- lib/activityobject.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index e6fefca28f..e1d5e25e5e 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -383,7 +383,10 @@ class Activity // updatedTime <-- should we use? spec says activity MAY have this // verb - $activity['verb'] = $this->verb; + // + // We can probably use the whole schema URL here but probably the + // relative simple name is easier to parse + $activity['verb'] = substr($this->verb, strrpos($this->verb, '/') + 1); // TODO: extensions (ActivityContext, OStatus stuff, etc.) diff --git a/lib/activityobject.php b/lib/activityobject.php index 17753f0df4..7847a5d640 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -683,7 +683,10 @@ class ActivityObject } // objectType - $object['type'] = $this->type; + // + // We can probably use the whole schema URL here but probably the + // relative simple name is easier to parse + $object['type'] = substr($this->type, strrpos($this->type, '/') + 1); // summary $object['summary'] = $this->summary; From b1675ff175cf54e4d9b97f24608d341551469ca8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Feb 2011 18:14:58 -0800 Subject: [PATCH 05/16] Rudimentary support for enclosures and some fiddling --- lib/activity.php | 62 +++++++++++++++++++++++++++--- lib/activityobject.php | 8 ++-- lib/activitystreamjsondocument.php | 2 +- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index e1d5e25e5e..6b3b121087 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -354,13 +354,58 @@ class Activity // body $activity['body'] = $this->content; - // generator <--- might be useful; might be too much junk + // generator <-- We should use this when we know a notice is created + // locally - // icon <-- should we use this? + // icon <-- Should we use this? Maybe a little bubble like we have + // on Facebook posts? // object if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { $activity['object'] = $this->objects[0]->asArray(); + + // Instead of adding enclosures as an extension to JSON + // Activities, it seems like we should be using the + // attachedObjects property of ActivityObject + + $attachedObjects = array(); + + // XXX: OK, this is kinda cheating. We should probably figure out + // what kind of objects these are based on mime-type and then + // create specific object types. Right now this rely on + // duck-typing. Also, we should include an embed code for + // video attachments. + + foreach ($this->enclosures as $enclosure) { + + if (is_string($enclosure)) { + + $attachedObjects[]['id'] = $enclosure; + + } else { + + $attachedObjects[]['id'] = $enclosure->url; + + $mediaLink = new ActivityStreamsMediaLink( + $enclosure->url, + null, + null, + $enclosure->mimetype + // XXX: Add 'size' as an extension to MediaLink? + ); + + $attachedObjects[]['mediaLink'] = $mediaLink->asArray(); // extension + + if ($enclosure->title) { + $attachedObjects[]['displayName'] = $enclosure->title; + } + } + } + + if (!empty($attachedObjects)) { + $activity['object']['attachedObjects'] = $attachedObjects; + } + } else { $activity['object'] = array(); foreach($this->objects as $object) { @@ -370,7 +415,8 @@ class Activity $activity['postedTime'] = self::iso8601Date($this->time); // Change to exactly be RFC3339? - // provider <--- again not sure we should use this + // provider <-- We should probably use this for showing the the source + // of remote notices, if known // target if (!empty($this->target)) { @@ -380,7 +426,8 @@ class Activity // title $activity['title'] = $this->title; - // updatedTime <-- should we use? spec says activity MAY have this + // updatedTime <-- Should we use this to indicate the time we received + // a remote notice? Probably not. // verb // @@ -388,7 +435,12 @@ class Activity // relative simple name is easier to parse $activity['verb'] = substr($this->verb, strrpos($this->verb, '/') + 1); - // TODO: extensions (ActivityContext, OStatus stuff, etc.) + /* Purely extensions hereafter */ + + if ($this->verb == ActivityVerb::POST) { + $activity['noticeInfo'] = $this->noticeInfo; + } + return array_filter($activity); } diff --git a/lib/activityobject.php b/lib/activityobject.php index 7847a5d640..d6356ed6fb 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -645,16 +645,18 @@ class ActivityObject { $object = array(); - // TODO: attachedObjects + // XXX: attachedObjects are added by Activity // displayName $object['displayName'] = $this->title; - // TODO: downstreamDuplicates - // TODO: embedCode (video) + + // embedCode (used for video) // id + // + // XXX: Should we use URL here? or a crazy tag URI? $object['id'] = $this->id; if ($this->type == ActivityObject::PERSON diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index 4fa456470e..00029f85f9 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -207,7 +207,7 @@ class ActivityStreamsLink $this->linkDict = array( 'url' => $url, 'rel' => $rel, // extension - 'media_type' => $mediaType // extension + 'mediaType' => $mediaType // extension ); } From a199192ebf67a8c2b961bab540918d8e19e8e47e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Feb 2011 18:55:13 -0800 Subject: [PATCH 06/16] Add avatars and notice info --- lib/activity.php | 10 +++++--- lib/activityobject.php | 40 ++++++++++++++++++++---------- lib/activitystreamjsondocument.php | 9 ++++--- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index 6b3b121087..b9bd8c31fe 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -437,10 +437,14 @@ class Activity /* Purely extensions hereafter */ - if ($this->verb == ActivityVerb::POST) { - $activity['noticeInfo'] = $this->noticeInfo; - } + // XXX: a bit of a hack... Since JSON isn't namespaced we probably + // shouldn't be using 'statusnet:notice_info', but this will work + // for the moment. + foreach ($this->extra as $e) { + list($objectName, $props, $txt) = $e; + $activity[$objectName] = $props; + } return array_filter($activity); } diff --git a/lib/activityobject.php b/lib/activityobject.php index d6356ed6fb..0df9999454 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -663,24 +663,33 @@ class ActivityObject || $this->type == ActivityObject::GROUP) { // XXX: Not sure what the best avatar is to use for the - // author's "image". For now, I'm using the stream size - // one, but possibly it should be large - $avatarLink = null; + // author's "image". For now, I'm using the large size. + + $avatarLarge = null; + $avatarMediaLinks = array(); foreach ($this->avatarLinks as $a) { - if ($a->height == AVATAR_STREAM_SIZE) { - $avatarLink = $a; - break; + + // Make a MediaLink for every other Avatar + $avatar = new ActivityStreamsMediaLink( + $a->url, + $a->width, + $a->height, + $a->type, + 'avatar' + ); + + // Find the big avatar to use as the "image" + if ($a->height == AVATAR_PROFILE_SIZE) { + $imgLink = $avatar; } + + $avatarMediaLinks[] = $avatar->asArray(); } - $imgLink = new ActivityStreamsMediaLink( - $avatarLink->url, - $avatarLink->width, - $avatarLink->height, - $avatarLink->type - ); + $object['avatars'] = $avatarMediaLinks; // extension + // image $object['image'] = $imgLink->asArray(); } @@ -698,7 +707,12 @@ class ActivityObject // url (XXX: need to put the right thing here...) $object['url'] = $this->id; - // TODO: extensions (OStatus stuff, etc.) + /* Extensions */ + + foreach ($this->extra as $e) { + list($objectName, $props, $txt) = $e; + $object[$objectName] = $props; + } return array_filter($object); } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index 00029f85f9..7e06652a45 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -77,6 +77,7 @@ class ActivityStreamJSONDocument * * @param String $title the title */ + function setTitle($title) { $this->doc['title'] = $title; @@ -205,10 +206,10 @@ class ActivityStreamsLink } $this->linkDict = array( - 'url' => $url, - 'rel' => $rel, // extension - 'mediaType' => $mediaType // extension - ); + 'url' => $url, + 'rel' => $rel, // extension + 'type' => $mediaType // extension + ); } function asArray() From 29ce5dd19a5f3d395aeddddaab0350f8818199b5 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 17 Feb 2011 19:02:57 -0800 Subject: [PATCH 07/16] Reinstate profile_info in author/actor --- classes/Notice.php | 12 +++++++----- classes/Profile.php | 4 ++-- lib/activity.php | 5 +++-- lib/activitystreamjsondocument.php | 3 +-- lib/atomusernoticefeed.php | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 007eb55f85..95b3d5b480 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1250,7 +1250,7 @@ class Notice extends Memcached_DataObject * @return Activity activity object representing this Notice. */ - function asActivity() + function asActivity($cur) { $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); @@ -1262,9 +1262,11 @@ class Notice extends Memcached_DataObject if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { $profile = $this->getProfile(); - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); + + $act->actor = ActivityObject::fromProfile($profile); + $act->actor->extra[] = $profile->profileInfo($cur); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); // XXX: should this be handled by default processing for object entry? @@ -1402,7 +1404,7 @@ class Notice extends Memcached_DataObject $author=true, $cur=null) { - $act = $this->asActivity(); + $act = $this->asActivity($cur); $act->extra[] = $this->noticeInfo($cur); return $act->asString($namespace, $author, $source); } diff --git a/classes/Profile.php b/classes/Profile.php index bdac3ba453..ad01581d5f 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -932,12 +932,12 @@ class Profile extends Memcached_DataObject * * @param User $cur Current user * - * @return array representation of element + * @return array representation of element or null */ function profileInfo($cur) { - $profileInfoAttr = array(); + $profileInfoAttr = array('local_id' => $this->id); if ($cur != null) { // Whether the current user is a subscribed to this profile diff --git a/lib/activity.php b/lib/activity.php index b9bd8c31fe..a8e6d25af9 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -443,9 +443,10 @@ class Activity foreach ($this->extra as $e) { list($objectName, $props, $txt) = $e; - $activity[$objectName] = $props; + if (!empty($objectName)) { + $activity[$objectName] = $props; + } } - return array_filter($activity); } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index 7e06652a45..f74bb6d2e6 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -92,7 +92,6 @@ class ActivityStreamJSONDocument function addItemsFromNotices($notices) { - common_debug("addItemsFromNotices"); if (is_array($notices)) { foreach ($notices as $notice) { $this->addItemFromNotice($notice); @@ -114,7 +113,7 @@ class ActivityStreamJSONDocument { $cur = empty($this->cur) ? common_current_user() : $this->cur; - $act = $notice->asActivity(); + $act = $notice->asActivity($cur); $act->extra[] = $notice->noticeInfo($cur); array_push($this->doc['items'], $act->asArray()); diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php index 3398cc8b4d..fb0ac5f831 100644 --- a/lib/atomusernoticefeed.php +++ b/lib/atomusernoticefeed.php @@ -64,7 +64,7 @@ class AtomUserNoticeFeed extends AtomNoticeFeed $ao = ActivityObject::fromProfile($profile); - $ao->extra[] = $profile->profileInfo($cur); + array_push($ao->extra, $profile->profileInfo($cur)); // XXX: For users, we generate an author _AND_ an // This is for backward compatibility with clients (especially From e84b01c76f5c5c35912bf0a4d6146e4af6511524 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 17 Feb 2011 19:34:39 -0800 Subject: [PATCH 08/16] avatars -> avatarLinks --- lib/activityobject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activityobject.php b/lib/activityobject.php index 0df9999454..0f151ec298 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -687,7 +687,7 @@ class ActivityObject $avatarMediaLinks[] = $avatar->asArray(); } - $object['avatars'] = $avatarMediaLinks; // extension + $object['avatarLinks'] = $avatarMediaLinks; // extension // image $object['image'] = $imgLink->asArray(); From 97af5e195421591265094685d9fa86af8d464794 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 17 Feb 2011 20:12:28 -0800 Subject: [PATCH 09/16] Add geopoint (GeoJSON) extension to AS JSON output --- lib/activity.php | 18 ++++++++++++++++++ lib/activityobject.php | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/activity.php b/lib/activity.php index a8e6d25af9..e2699ebadc 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -447,6 +447,24 @@ class Activity $activity[$objectName] = $props; } } + + /* more extensions */ + + if (!empty($this->context)) { + + if (!empty($this->context->location)) { + $loc = $this->context->location; + + // GeoJSON + + $activity['geopoint'] = array( + 'type' => 'Point', + 'coordinates' => array($loc->lat, $loc->lon) + ); + } + + } + return array_filter($activity); } diff --git a/lib/activityobject.php b/lib/activityobject.php index 0f151ec298..ae2f4649e5 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -714,6 +714,18 @@ class ActivityObject $object[$objectName] = $props; } + // GeoJSON + + if (!empty($this->geopoint)) { + + list($lat, $long) = explode(' ', $this->geopoint); + + $object['geopoint'] = array( + 'type' => 'Point', + 'coordinates' => array($lat, $long) + ); + } + return array_filter($object); } } From 11f77b2fcaf2b029fb0a298253bb99b0d96cd7b8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 17 Feb 2011 22:36:14 -0800 Subject: [PATCH 10/16] Add PoCo to Activity Streams JSON --- lib/activityobject.php | 4 ++++ lib/activitystreamjsondocument.php | 1 - lib/poco.php | 38 ++++++++++++++++++++++++++++++ lib/pocoaddress.php | 13 ++++++++++ lib/pocourl.php | 20 ++++++++++++++++ 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/activityobject.php b/lib/activityobject.php index ae2f4649e5..a69e1a1b42 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -726,6 +726,10 @@ class ActivityObject ); } + if (!empty($this->poco)) { + $object['contact'] = $this->poco->asArray(); + } + return array_filter($object); } } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index f74bb6d2e6..2b99d19eb7 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -115,7 +115,6 @@ class ActivityStreamJSONDocument $act = $notice->asActivity($cur); $act->extra[] = $notice->noticeInfo($cur); - array_push($this->doc['items'], $act->asArray()); } diff --git a/lib/poco.php b/lib/poco.php index d7b082163e..baea5b33b0 100644 --- a/lib/poco.php +++ b/lib/poco.php @@ -241,4 +241,42 @@ class PoCo $url->outputTo($xo); } } + + /** + * Output a Portable Contact as an array suitable for serializing + * as JSON + * + * @return $array the PoCo array + */ + + function asArray() + { + $poco = array(); + + $poco['preferredUsername'] = $this->preferredUsername; + $poco['displayName'] = $this->displayName; + + if (!empty($this->note)) { + $poco['note'] = $this->note; + } + + if (!empty($this->address)) { + $poco['addresses'] = $this->address->asArray(); + } + + if (!empty($this->urls)) { + + $urls = array(); + + foreach ($this->urls as $url) { + $urls[] = $url->asArray(); + } + + $poco['urls'] = $urls; + } + + return $poco; + } + } + diff --git a/lib/pocoaddress.php b/lib/pocoaddress.php index d9f6ff2bde..22d4d02b13 100644 --- a/lib/pocoaddress.php +++ b/lib/pocoaddress.php @@ -56,4 +56,17 @@ class PoCoAddress $xo->elementEnd('poco:address'); } } + + /** + * Return this PoCo address as an array suitable for serializing in JSON + * + * @return array the address + */ + + function asArray() + { + if (!empty($this->formatted)) { + return array('formatted' => $this->formatted); + } + } } diff --git a/lib/pocourl.php b/lib/pocourl.php index 786793b280..e375f125a0 100644 --- a/lib/pocourl.php +++ b/lib/pocourl.php @@ -67,4 +67,24 @@ class PoCoURL } $xo->elementEnd('poco:urls'); } + + /** + * Return this PoCo URL as an array suitable for serializing in JSON + * + * @array $url the url + */ + + function asArray() + { + $url = array(); + + $url['type'] = $this->type; + $url['value'] = $this->value; + + if (!empty($this->primary)) { + $url['primary'] = 'true'; + } + + return $url; + } } From 60f62199a1f65ba0192430034980d5ca49a7da1f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 00:51:08 -0800 Subject: [PATCH 11/16] Add context to Activity Streams JSON --- lib/activity.php | 6 ++++++ lib/activitycontext.php | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/activity.php b/lib/activity.php index e2699ebadc..218c965c4d 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -450,6 +450,9 @@ class Activity /* more extensions */ + // Context stuff. For now I'm just sticking most of it + // in a property called "context" + if (!empty($this->context)) { if (!empty($this->context->location)) { @@ -461,8 +464,11 @@ class Activity 'type' => 'Point', 'coordinates' => array($loc->lat, $loc->lon) ); + } + $activity['to'] = $this->context->getToArray(); + $activity['context'] = $this->context->asArray(); } return array_filter($activity); diff --git a/lib/activitycontext.php b/lib/activitycontext.php index fd0dfe06c1..3dc53d869d 100644 --- a/lib/activitycontext.php +++ b/lib/activitycontext.php @@ -130,4 +130,50 @@ class ActivityContext common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); return null; } + + /** + * Returns context (StatusNet stuff) as an array suitable for serializing + * in JSON. Right now context stuff is an extension to Activity. + * + * @return array the context + */ + function asArray() + { + $context = array(); + + $context['replyToId'] = $this->replyToID; + $context['replyToUrl'] = $this->replyToUrl; + $context['conversation'] = $this->conversation; + $context['forwardId'] = $this->forwardID; + $context['forwardUrl'] = $this->forwardUrl; + + // XXX: We might want to have the attention to stuff + // in here like we do with Atom + + return array_filter($context); + } + + /** + * Returns an array of arrays representing Activity Objects (intended to be + * serialized in JSON) that represent WHO the Activity is supposed to + * be received by. This is not really specified but appears in an example + * of the current spec as an extension. We might want to figure out a JSON + * serialization for OStatus and use that to express mentions instead. + * + * XXX: People's ideas on how to do this are all over the place + * + * @return array the array of recipients + */ + function getToArray() + { + $tos = array(); + + foreach ($this->attention as $attnUrl) { + $to = array('id' => $attnUrl, 'url' => $attnUrl); + $tos[] = $to; + } + + return $tos; + } + } From 20e414665d418edd5d62f5863a36e38a6c2f2284 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 09:29:49 -0800 Subject: [PATCH 12/16] Add tags to Activity Streams JSON --- lib/activity.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index 218c965c4d..25d70c982b 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -437,6 +437,15 @@ class Activity /* Purely extensions hereafter */ + $tags = array(); + + // Use an Activity Object for term? Which object? Note? + foreach ($this->categories as $cat) { + $tags[] = $cat->term; + } + + $activity['tags'] = $tags; + // XXX: a bit of a hack... Since JSON isn't namespaced we probably // shouldn't be using 'statusnet:notice_info', but this will work // for the moment. @@ -448,12 +457,11 @@ class Activity } } - /* more extensions */ // Context stuff. For now I'm just sticking most of it // in a property called "context" - if (!empty($this->context)) { + if (!empty($this->context)) { if (!empty($this->context->location)) { $loc = $this->context->location; From da42d36d7f42c2357c21f71034f9c1cd04220c99 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 10:02:41 -0800 Subject: [PATCH 13/16] Activity Streams JSON - express to and in-reply-to stuff as person objects --- lib/activity.php | 43 ++++++++++++++++++++--------------------- lib/activitycontext.php | 31 +++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index 25d70c982b..11af98abb5 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -364,6 +364,27 @@ class Activity if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { $activity['object'] = $this->objects[0]->asArray(); + // Context stuff. For now I'm just sticking most of it + // in a property called "context" + + if (!empty($this->context)) { + + if (!empty($this->context->location)) { + $loc = $this->context->location; + + // GeoJSON + + $activity['geopoint'] = array( + 'type' => 'Point', + 'coordinates' => array($loc->lat, $loc->lon) + ); + + } + + $activity['to'] = $this->context->getToArray(); + $activity['context'] = $this->context->asArray(); + } + // Instead of adding enclosures as an extension to JSON // Activities, it seems like we should be using the // attachedObjects property of ActivityObject @@ -457,28 +478,6 @@ class Activity } } - - // Context stuff. For now I'm just sticking most of it - // in a property called "context" - - if (!empty($this->context)) { - - if (!empty($this->context->location)) { - $loc = $this->context->location; - - // GeoJSON - - $activity['geopoint'] = array( - 'type' => 'Point', - 'coordinates' => array($loc->lat, $loc->lon) - ); - - } - - $activity['to'] = $this->context->getToArray(); - $activity['context'] = $this->context->asArray(); - } - return array_filter($activity); } diff --git a/lib/activitycontext.php b/lib/activitycontext.php index 3dc53d869d..acbd0e599f 100644 --- a/lib/activitycontext.php +++ b/lib/activitycontext.php @@ -137,19 +137,16 @@ class ActivityContext * * @return array the context */ + function asArray() { $context = array(); - $context['replyToId'] = $this->replyToID; - $context['replyToUrl'] = $this->replyToUrl; + $context['replyTo'] = $this->getInReplyToArray(); $context['conversation'] = $this->conversation; $context['forwardId'] = $this->forwardID; $context['forwardUrl'] = $this->forwardUrl; - // XXX: We might want to have the attention to stuff - // in here like we do with Atom - return array_filter($context); } @@ -164,16 +161,38 @@ class ActivityContext * * @return array the array of recipients */ + function getToArray() { $tos = array(); foreach ($this->attention as $attnUrl) { - $to = array('id' => $attnUrl, 'url' => $attnUrl); + $to = array( + 'objectType' => 'person', + 'id' => $attnUrl, + 'url' => $attnUrl + ); $tos[] = $to; } return $tos; } + /* + * Show replyTo + */ + + function getInReplyToArray() + { + $replyToObj = array('objectType' => 'note'); + + $replyToObj['id'] = $this->replyToID; + + if (!empty($this->replyToUrl)) { + $replyToObj['url'] = $this->replyToUrl; + } + + } + } + From 68017392ffd35ce93defb4600a17f9d7203b753f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 15:43:40 -0800 Subject: [PATCH 14/16] Make other timeline API endpoints output Activity Streams JSON --- actions/apitimelinefavorites.php | 8 ++++ actions/apitimelinegroup.php | 25 ++++++----- actions/apitimelinehome.php | 8 ++++ actions/apitimelinementions.php | 8 ++++ actions/apitimelinepublic.php | 8 ++++ actions/apitimelineretweetedtome.php | 45 +++++++++++++++---- actions/apitimelineretweetsofme.php | 65 +++++++++++++--------------- actions/apitimelinetag.php | 12 +++-- actions/apitimelineuser.php | 11 +++++ lib/router.php | 34 +++++++-------- 10 files changed, 150 insertions(+), 74 deletions(-) diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php index c952c46238..36fc3089f5 100644 --- a/actions/apitimelinefavorites.php +++ b/actions/apitimelinefavorites.php @@ -169,6 +169,14 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link,'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index e1bc102e45..3fc930fa08 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -106,6 +106,11 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $self = $this->getSelfUri(); + $link = common_local_url( + 'ApiTimelineGroup', + array('nickname' => $this->group->nickname) + ); + switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); @@ -123,24 +128,20 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction break; case 'atom': header('Content-Type: application/atom+xml; charset=utf-8'); - - try { $atom->addEntryFromNotices($this->notices); $this->raw($atom->getString()); - } catch (Atom10FeedException $e) { - $this->serverError( - // TRANS: Server error displayed when generating an Atom feed fails. - // TRANS: %s is the error. - sprintf(_('Could not generate feed for group - %s'),$e->getMessage()), - 400, - $this->format - ); - return; - } break; case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($atom->title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: $this->clientError( // TRANS: Client error displayed when trying to handle an unknown API method. diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php index 75a9f72580..023c9698a1 100644 --- a/actions/apitimelinehome.php +++ b/actions/apitimelinehome.php @@ -168,6 +168,14 @@ class ApiTimelineHomeAction extends ApiBareAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php index a9b6d0b3df..2857bd41ea 100644 --- a/actions/apitimelinementions.php +++ b/actions/apitimelinementions.php @@ -169,6 +169,14 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index 2745e5d3fe..353973b653 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -234,6 +234,14 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/actions/apitimelineretweetedtome.php b/actions/apitimelineretweetedtome.php index 6213a08eac..b9f9be1dda 100644 --- a/actions/apitimelineretweetedtome.php +++ b/actions/apitimelineretweetedtome.php @@ -92,6 +92,20 @@ class ApiTimelineRetweetedToMeAction extends ApiAuthAction $offset = ($this->page-1) * $this->cnt; $limit = $this->cnt; + // TRANS: Title for Atom feed "repeated to me". %s is the user nickname. + $title = sprintf(_("Repeated to %s"), $this->auth_user->nickname); + $subtitle = sprintf( + _('%1$s notices that were to repeated to %2$s / %3$s.'), + $sitename, $this->user->nickname, $profile->getBestName() + ); + $taguribase = TagURI::base(); + $id = "tag:$taguribase:RepeatedToMe:" . $this->auth_user->id; + + $link = common_local_url( + 'all', + array('nickname' => $this->auth_user->nickname) + ); + $strm = $this->auth_user->repeatedToMe($offset, $limit, $this->since_id, $this->max_id); switch ($this->format) { @@ -102,16 +116,31 @@ class ApiTimelineRetweetedToMeAction extends ApiAuthAction $this->showJsonTimeline($strm); break; case 'atom': - $profile = $this->auth_user->getProfile(); + header('Content-Type: application/atom+xml; charset=utf-8'); - // TRANS: Title for Atom feed "repeated to me". %s is the user nickname. - $title = sprintf(_("Repeated to %s"), $this->auth_user->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:RepeatedToMe:" . $this->auth_user->id; - $link = common_local_url('all', - array('nickname' => $this->auth_user->nickname)); + $atom = new AtomNoticeFeed($this->auth_user); - $this->showAtomTimeline($strm, $title, $id, $link); + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setUpdated('now'); + $atom->addLink($link); + + $id = $this->arg('id'); + + $atom->setSelfLink($self); + $atom->addEntryFromNotices($strm); + + $this->raw($atom->getString()); + + break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($strm); + $this->raw($doc->asString()); break; default: // TRANS: Client error displayed when trying to handle an unknown API method. diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php index 9cb277279f..aec6877f15 100644 --- a/actions/apitimelineretweetsofme.php +++ b/actions/apitimelineretweetsofme.php @@ -93,9 +93,27 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $offset = ($this->page-1) * $this->cnt; $limit = $this->cnt; - $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); + // TRANS: Title of list of repeated notices of the logged in user. + // TRANS: %s is the nickname of the logged in user. + $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); + $sitename = common_config('site', 'name'); - common_debug(var_export($strm, true)); + $profile = $this->auth_user->getProfile(); + + $subtitle = sprintf( + _('%1$s notices that %2$s / %3$s has repeated.'), + $sitename, $this->auth_user->nickname, $profile->getBestName() + ); + + $taguribase = TagURI::base(); + $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; + + $link = common_local_url( + 'all', + array('nickname' => $this->auth_user->nickname) + ); + + $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); switch ($this->format) { case 'xml': @@ -105,49 +123,28 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $this->showJsonTimeline($strm); break; case 'atom': - $profile = $this->auth_user->getProfile(); - - // TRANS: Title of list of repeated notices of the logged in user. - // TRANS: %s is the nickname of the logged in user. - $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; - header('Content-Type: application/atom+xml; charset=utf-8'); - $atom = new AtomNoticeFeed($this->auth_user); - $atom->setId($id); $atom->setTitle($title); $atom->setSubtitle($subtitle); $atom->setUpdated('now'); - - $atom->addLink( - common_local_url( - 'showstream', - array('nickname' => $this->auth_user->nickname) - ) - ); - - $id = $this->arg('id'); - $aargs = array('format' => 'atom'); - if (!empty($id)) { - $aargs['id'] = $id; - } - - $atom->addLink( - $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs), - array('rel' => 'self', 'type' => 'application/atom+xml') - ); - + $atom->addLink($link); + $atom->setSelfLink($this->getSelfUri()); $atom->addEntryFromNotices($strm); - $this->raw($atom->getString()); - + break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($strm); + $this->raw($doc->asString()); break; default: // TRANS: Client error displayed when trying to handle an unknown API method. - $this->clientError(_('API method not found.'), $code = 404); + $this->clientError(_('API method not found.'), 404); break; } } diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 4dbe1fc0db..5fa76d0cd0 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -107,7 +107,7 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $sitename ); $taguribase = TagURI::base(); - $id = "tag:$taguribase:TagTimeline:".$tag; + $id = "tag:$taguribase:TagTimeline:".$this->tag; $link = common_local_url( 'tag', @@ -116,8 +116,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $self = $this->getSelfUri(); - common_debug("self link is: $self"); - switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); @@ -154,6 +152,14 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index b0ca7e923f..66984b5abd 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -201,6 +201,17 @@ class ApiTimelineUserAction extends ApiBareAuthAction case 'json': $this->showJsonTimeline($this->notices); break; + case 'as': + header('Content-Type: application/json; charset=utf-8'); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($atom->title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + + // XXX: Add paging extension? + + $this->raw($doc->asString()); + break; default: // TRANS: Client error displayed when trying to handle an unknown API method. $this->clientError(_('API method not found.'), $code = 404); diff --git a/lib/router.php b/lib/router.php index 9f2b1df868..a4547b3258 100644 --- a/lib/router.php +++ b/lib/router.php @@ -407,7 +407,7 @@ class Router $m->connect('api/statuses/public_timeline.:format', array('action' => 'ApiTimelinePublic', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/friends_timeline.:format', array('action' => 'ApiTimelineFriends', @@ -416,55 +416,55 @@ class Router $m->connect('api/statuses/friends_timeline/:id.:format', array('action' => 'ApiTimelineFriends', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/home_timeline.:format', array('action' => 'ApiTimelineHome', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/home_timeline/:id.:format', array('action' => 'ApiTimelineHome', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/user_timeline.:format', array('action' => 'ApiTimelineUser', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/user_timeline/:id.:format', array('action' => 'ApiTimelineUser', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/mentions.:format', array('action' => 'ApiTimelineMentions', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/mentions/:id.:format', array('action' => 'ApiTimelineMentions', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/replies.:format', array('action' => 'ApiTimelineMentions', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/replies/:id.:format', array('action' => 'ApiTimelineMentions', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statuses/retweeted_by_me.:format', array('action' => 'ApiTimelineRetweetedByMe', - 'format' => '(xml|json|atom)')); + 'format' => '(xml|json|atom|as)')); $m->connect('api/statuses/retweeted_to_me.:format', array('action' => 'ApiTimelineRetweetedToMe', - 'format' => '(xml|json|atom)')); + 'format' => '(xml|json|atom|as)')); $m->connect('api/statuses/retweets_of_me.:format', array('action' => 'ApiTimelineRetweetsOfMe', - 'format' => '(xml|json|atom)')); + 'format' => '(xml|json|atom|as)')); $m->connect('api/statuses/friends.:format', array('action' => 'ApiUserFriends', @@ -625,12 +625,12 @@ class Router $m->connect('api/favorites.:format', array('action' => 'ApiTimelineFavorites', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/favorites/:id.:format', array('action' => 'ApiTimelineFavorites', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/favorites/create/:id.:format', array('action' => 'ApiFavoriteCreate', @@ -695,7 +695,7 @@ class Router $m->connect('api/statusnet/groups/timeline/:id.:format', array('action' => 'ApiTimelineGroup', 'id' => Nickname::INPUT_FMT, - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); $m->connect('api/statusnet/groups/show.:format', array('action' => 'ApiGroupShow', @@ -756,7 +756,7 @@ class Router // Tags $m->connect('api/statusnet/tags/timeline/:tag.:format', array('action' => 'ApiTimelineTag', - 'format' => '(xml|json|rss|atom)')); + 'format' => '(xml|json|rss|atom|as)')); // media related $m->connect( From b741184d9b8fc5cd341b0a7e24f02cf8a176d4b7 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 18:10:21 -0800 Subject: [PATCH 15/16] Add provider to Activity Streams JSON --- lib/activity.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/activity.php b/lib/activity.php index 11af98abb5..b781e49846 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -354,11 +354,12 @@ class Activity // body $activity['body'] = $this->content; - // generator <-- We should use this when we know a notice is created - // locally + // generator <-- We could use this when we know a notice is created + // locally. Or if we know the upstream Generator. + + // icon <-- I've decided to use the posting user's stream avatar here + // for now (also included in the avatarLinks extension) - // icon <-- Should we use this? Maybe a little bubble like we have - // on Facebook posts? // object if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { @@ -436,8 +437,14 @@ class Activity $activity['postedTime'] = self::iso8601Date($this->time); // Change to exactly be RFC3339? - // provider <-- We should probably use this for showing the the source - // of remote notices, if known + // provider + $provider = array( + 'objectType' => 'service', + 'displayName' => common_config('site', 'name'), + 'url' => common_root_url() + ); + + $activity['provider'] = $provider; // target if (!empty($this->target)) { From 93e3d49044029856d4c9ce3710385dc5154084a1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 18 Feb 2011 18:52:49 -0800 Subject: [PATCH 16/16] Activity Streams JSON: inReplyTo objects weren't being output. Fixed. --- lib/activitycontext.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/activitycontext.php b/lib/activitycontext.php index acbd0e599f..2eff3fb15f 100644 --- a/lib/activitycontext.php +++ b/lib/activitycontext.php @@ -142,7 +142,7 @@ class ActivityContext { $context = array(); - $context['replyTo'] = $this->getInReplyToArray(); + $context['inReplyTo'] = $this->getInReplyToArray(); $context['conversation'] = $this->conversation; $context['forwardId'] = $this->forwardID; $context['forwardUrl'] = $this->forwardUrl; @@ -178,20 +178,31 @@ class ActivityContext return $tos; } - /* - * Show replyTo + /** + * Return an array for the notices this notice is a reply to + * suitable for serializing as JSON note objects. + * + * @return array the array of notes */ function getInReplyToArray() { + if (empty($this->replyToID) && empty($this->replyToUrl)) { + return null; + } + $replyToObj = array('objectType' => 'note'); - $replyToObj['id'] = $this->replyToID; - + // XXX: Possibly shorten this to just the numeric ID? + // Currently, it's the full URI of the notice. + if (!empty($this->replyToID)) { + $replyToObj['id'] = $this->replyToID; + } if (!empty($this->replyToUrl)) { $replyToObj['url'] = $this->replyToUrl; } + return $replyToObj; } }