Commit 17176ee4 authored by Zach Copley's avatar Zach Copley

Merge branch 'json-activities' into 0.9.x

parents f743a233 93e3d490
......@@ -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);
......
......@@ -263,6 +263,14 @@ 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->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);
......
......@@ -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.
......
......@@ -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);
......
......@@ -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);
......
......@@ -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);
......
......@@ -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');
$atom = new AtomNoticeFeed($this->auth_user);
$atom->setId($id);
$atom->setTitle($title);
$atom->setSubtitle($subtitle);
$atom->setUpdated('now');
$atom->addLink($link);
// 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));
$id = $this->arg('id');
$this->showAtomTimeline($strm, $title, $id, $link);
$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.
......
......@@ -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');
$profile = $this->auth_user->getProfile();
$subtitle = sprintf(
_('%1$s notices that %2$s / %3$s has repeated.'),
$sitename, $this->auth_user->nickname, $profile->getBestName()
);
common_debug(var_export($strm, true));
$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;
}
}
......
......@@ -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);
......
......@@ -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);
......
......@@ -1250,23 +1250,23 @@ 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));
if (!empty($act)) {
return $act;
}
$act = new Activity();
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?
......@@ -1404,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);
}
......
......@@ -946,12 +946,12 @@ class Profile extends Memcached_DataObject
*
* @param User $cur Current user
*
* @return array representation of <statusnet:profile_info> element
* @return array representation of <statusnet:profile_info> 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
......
......@@ -337,6 +337,157 @@ 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 <-- 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)
// object
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
$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) {
$activity['object'][] = $object->asArray();
}
}
$activity['postedTime'] = self::iso8601Date($this->time); // Change to exactly be RFC3339?
// provider
$provider = array(
'objectType' => 'service',
'displayName' => common_config('site', 'name'),
'url' => common_root_url()
);
$activity['provider'] = $provider;
// target
if (!empty($this->target)) {
$activity['target'] = $this->target->asArray();
}
// title
$activity['title'] = $this->title;
// updatedTime <-- Should we use this to indicate the time we received
// a remote notice? Probably not.
// 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);
/* 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.
foreach ($this->extra as $e) {
list($objectName, $props, $txt) = $e;
if (!empty($objectName)) {
$activity[$objectName] = $props;
}
}
return array_filter($activity);
}
function asString($namespace=false, $author=true, $source=false)
{
$xs = new XMLStringer(true);
......
......@@ -130,4 +130,80 @@ 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['inReplyTo'] = $this->getInReplyToArray();
$context['conversation'] = $this->conversation;
$context['forwardId'] = $this->forwardID;
$context['forwardUrl'] = $this->forwardUrl;
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(
'objectType' => 'person',
'id' => $attnUrl,
'url' => $attnUrl
);
$tos[] = $to;
}
return $tos;
}
/**
* 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');
// 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;
}
}
......@@ -179,7 +179,7 @@ class ActivityObject
if (empty($this->type)) {
$this->type = self::PERSON; // XXX: is this fair?
}
// start with <atom:title>
$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,103 @@ 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();
// XXX: attachedObjects are added by Activity
// displayName
$object['displayName'] = $this->title;
// TODO: downstreamDuplicates
// 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
|| $this->type == ActivityObject::GROUP) {