Merge branch 'atomic-json' into 1.0.x

This commit is contained in:
Brion Vibber 2011-03-11 12:47:11 -08:00
commit 3c9561b3f0
4 changed files with 366 additions and 217 deletions

View File

@ -1115,3 +1115,19 @@ StartGroupProfileElements: Start showing stuff about the group on its profile pa
EndGroupProfileElements: Start showing stuff about the group on its profile page EndGroupProfileElements: Start showing stuff about the group on its profile page
- $action: action being executed (for output and params) - $action: action being executed (for output and params)
- $group: group for the page - $group: group for the page
StartActivityObjectOutputAtom: Called at start of Atom XML output generation for ActivityObject chunks, just inside the <activity:object>. Cancel the event to take over its output completely (you're responsible for calling the matching End event if so)
- $obj: ActivityObject
- $out: XMLOutputter to append custom output
EndActivityObjectOutputAtom: Called at end of Atom XML output generation for ActivityObject chunks, just inside the </activity:object>
- $obj: ActivityObject
- $out: XMLOutputter to append custom output
StartActivityObjectOutputJson: Called at start of JSON output generation for ActivityObject chunks: the array has not yet been filled out. Cancel the event to take over its output completely (you're responsible for calling the matching End event if so)
- $obj ActivityObject
- &$out: array to be serialized; you're free to modify it
EndActivityObjectOutputJson: Called at end of JSON output generation for ActivityObject chunks: the array has not yet been filled out.
- $obj ActivityObject
- &$out: array to be serialized; you're free to modify it

View File

@ -533,91 +533,95 @@ class ActivityObject
$xo->elementStart($tag); $xo->elementStart($tag);
} }
$xo->element('activity:object-type', null, $this->type); if (Event::handle('StartActivityObjectOutputAtom', array($this, $xo))) {
$xo->element('activity:object-type', null, $this->type);
// <author> uses URI // <author> uses URI
if ($tag == 'author') {
$xo->element(self::URI, null, $this->id);
} else {
$xo->element(self::ID, null, $this->id);
}
if (!empty($this->title)) {
$name = common_xml_safe_str($this->title);
if ($tag == 'author') { if ($tag == 'author') {
// XXX: Backward compatibility hack -- atom:name should contain $xo->element(self::URI, null, $this->id);
// full name here, instead of nickname, i.e.: $name. Change
// this in the next version.
$xo->element(self::NAME, null, $this->poco->preferredUsername);
} else { } else {
$xo->element(self::TITLE, null, $name); $xo->element(self::ID, null, $this->id);
} }
}
if (!empty($this->summary)) { if (!empty($this->title)) {
$xo->element( $name = common_xml_safe_str($this->title);
self::SUMMARY, if ($tag == 'author') {
null, // XXX: Backward compatibility hack -- atom:name should contain
common_xml_safe_str($this->summary) // full name here, instead of nickname, i.e.: $name. Change
); // this in the next version.
} $xo->element(self::NAME, null, $this->poco->preferredUsername);
} else {
$xo->element(self::TITLE, null, $name);
}
}
if (!empty($this->content)) { if (!empty($this->summary)) {
// XXX: assuming HTML content here
$xo->element(
ActivityUtils::CONTENT,
array('type' => 'html'),
common_xml_safe_str($this->content)
);
}
if (!empty($this->link)) {
$xo->element(
'link',
array(
'rel' => 'alternate',
'type' => 'text/html',
'href' => $this->link
),
null
);
}
if ($this->type == ActivityObject::PERSON
|| $this->type == ActivityObject::GROUP) {
foreach ($this->avatarLinks as $avatar) {
$xo->element( $xo->element(
'link', array( self::SUMMARY,
'rel' => 'avatar', null,
'type' => $avatar->type, common_xml_safe_str($this->summary)
'media:width' => $avatar->width, );
'media:height' => $avatar->height, }
'href' => $avatar->url
if (!empty($this->content)) {
// XXX: assuming HTML content here
$xo->element(
ActivityUtils::CONTENT,
array('type' => 'html'),
common_xml_safe_str($this->content)
);
}
if (!empty($this->link)) {
$xo->element(
'link',
array(
'rel' => 'alternate',
'type' => 'text/html',
'href' => $this->link
), ),
null null
); );
} }
}
if (!empty($this->geopoint)) { if ($this->type == ActivityObject::PERSON
$xo->element( || $this->type == ActivityObject::GROUP) {
'georss:point',
null,
$this->geopoint
);
}
if (!empty($this->poco)) { foreach ($this->avatarLinks as $avatar) {
$this->poco->outputTo($xo); $xo->element(
} 'link', array(
'rel' => 'avatar',
'type' => $avatar->type,
'media:width' => $avatar->width,
'media:height' => $avatar->height,
'href' => $avatar->url
),
null
);
}
}
// @fixme there's no way here to make a tree; elements can only contain plaintext if (!empty($this->geopoint)) {
// @fixme these may collide with JSON extensions $xo->element(
foreach ($this->extra as $el) { 'georss:point',
list($extraTag, $attrs, $content) = $el; null,
$xo->element($extraTag, $attrs, $content); $this->geopoint
);
}
if (!empty($this->poco)) {
$this->poco->outputTo($xo);
}
// @fixme there's no way here to make a tree; elements can only contain plaintext
// @fixme these may collide with JSON extensions
foreach ($this->extra as $el) {
list($extraTag, $attrs, $content) = $el;
$xo->element($extraTag, $attrs, $content);
}
Event::handle('EndActivityObjectOutputAtom', array($this, $xo));
} }
if (!empty($tag)) { if (!empty($tag)) {
@ -647,94 +651,96 @@ class ActivityObject
{ {
$object = array(); $object = array();
// XXX: attachedObjects are added by Activity if (Event::handle('StartActivityObjectOutputJson', array($this, &$object))) {
// XXX: attachedObjects are added by Activity
// displayName // displayName
$object['displayName'] = $this->title; $object['displayName'] = $this->title;
// TODO: downstreamDuplicates // TODO: downstreamDuplicates
// embedCode (used for video) // embedCode (used for video)
// id // id
// //
// XXX: Should we use URL here? or a crazy tag URI? // XXX: Should we use URL here? or a crazy tag URI?
$object['id'] = $this->id; $object['id'] = $this->id;
if ($this->type == ActivityObject::PERSON if ($this->type == ActivityObject::PERSON
|| $this->type == ActivityObject::GROUP) { || $this->type == ActivityObject::GROUP) {
// XXX: Not sure what the best avatar is to use for the // XXX: Not sure what the best avatar is to use for the
// author's "image". For now, I'm using the large size. // author's "image". For now, I'm using the large size.
$avatarLarge = null; $avatarLarge = null;
$avatarMediaLinks = array(); $avatarMediaLinks = array();
foreach ($this->avatarLinks as $a) { foreach ($this->avatarLinks as $a) {
// Make a MediaLink for every other Avatar // Make a MediaLink for every other Avatar
$avatar = new ActivityStreamsMediaLink( $avatar = new ActivityStreamsMediaLink(
$a->url, $a->url,
$a->width, $a->width,
$a->height, $a->height,
$a->type, $a->type,
'avatar' 'avatar'
); );
// Find the big avatar to use as the "image" // Find the big avatar to use as the "image"
if ($a->height == AVATAR_PROFILE_SIZE) { if ($a->height == AVATAR_PROFILE_SIZE) {
$imgLink = $avatar; $imgLink = $avatar;
}
$avatarMediaLinks[] = $avatar->asArray();
} }
$avatarMediaLinks[] = $avatar->asArray(); $object['avatarLinks'] = $avatarMediaLinks; // extension
// image
$object['image'] = $imgLink->asArray();
} }
$object['avatarLinks'] = $avatarMediaLinks; // extension // objectType
//
// We can probably use the whole schema URL here but probably the
// relative simple name is easier to parse
// @fixme this breaks extension URIs
$object['type'] = substr($this->type, strrpos($this->type, '/') + 1);
// image // summary
$object['image'] = $imgLink->asArray(); $object['summary'] = $this->summary;
// TODO: upstreamDuplicates
// url (XXX: need to put the right thing here...)
$object['url'] = $this->id;
/* Extensions */
// @fixme these may collide with XML extensions
// @fixme multiple tags of same name will overwrite each other
// @fixme text content from XML extensions will be lost
foreach ($this->extra as $e) {
list($objectName, $props, $txt) = $e;
$object[$objectName] = $props;
}
// GeoJSON
if (!empty($this->geopoint)) {
list($lat, $long) = explode(' ', $this->geopoint);
$object['geopoint'] = array(
'type' => 'Point',
'coordinates' => array($lat, $long)
);
}
if (!empty($this->poco)) {
$object['contact'] = $this->poco->asArray();
}
Event::handle('EndActivityObjectOutputJson', array($this, &$object));
} }
// objectType
//
// We can probably use the whole schema URL here but probably the
// relative simple name is easier to parse
// @fixme this breaks extension URIs
$object['type'] = substr($this->type, strrpos($this->type, '/') + 1);
// summary
$object['summary'] = $this->summary;
// TODO: upstreamDuplicates
// url (XXX: need to put the right thing here...)
$object['url'] = $this->id;
/* Extensions */
// @fixme these may collide with XML extensions
// @fixme multiple tags of same name will overwrite each other
// @fixme text content from XML extensions will be lost
foreach ($this->extra as $e) {
list($objectName, $props, $txt) = $e;
$object[$objectName] = $props;
}
// GeoJSON
if (!empty($this->geopoint)) {
list($lat, $long) = explode(' ', $this->geopoint);
$object['geopoint'] = array(
'type' => 'Point',
'coordinates' => array($lat, $long)
);
}
if (!empty($this->poco)) {
$object['contact'] = $this->poco->asArray();
}
return array_filter($object); return array_filter($object);
} }
} }

View File

@ -212,6 +212,44 @@ abstract class MicroAppPlugin extends Plugin
in_array($activity->objects[0]->type, $types)); in_array($activity->objects[0]->type, $types));
} }
/**
* Called when generating Atom XML ActivityStreams output from an
* ActivityObject belonging to this plugin. Gives the plugin
* a chance to add custom output.
*
* Note that you can only add output of additional XML elements,
* not change existing stuff here.
*
* If output is already handled by the base Activity classes,
* you can leave this base implementation as a no-op.
*
* @param ActivityObject $obj
* @param XMLOutputter $out to add elements at end of object
*/
function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
{
// default is a no-op
}
/**
* Called when generating JSON ActivityStreams output from an
* ActivityObject belonging to this plugin. Gives the plugin
* a chance to add custom output.
*
* Modify the array contents to your heart's content, and it'll
* all get serialized out as JSON.
*
* If output is already handled by the base Activity classes,
* you can leave this base implementation as a no-op.
*
* @param ActivityObject $obj
* @param array &$out JSON-targeted array which can be modified
*/
public function activityObjectOutputJson(ActivityObject $obj, array &$out)
{
// default is a no-op
}
/** /**
* When a notice is deleted, delete the related objects * When a notice is deleted, delete the related objects
* by calling the overridable $this->deleteRelated(). * by calling the overridable $this->deleteRelated().
@ -439,6 +477,46 @@ abstract class MicroAppPlugin extends Plugin
return true; return true;
} }
/**
* Event handler gives the plugin a chance to add custom
* Atom XML ActivityStreams output from a previously filled-out
* ActivityObject.
*
* The atomOutput method is called if it's one of
* our matching types.
*
* @param ActivityObject $obj
* @param XMLOutputter $out to add elements at end of object
* @return boolean hook return value
*/
function onEndActivityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
{
if (in_array($obj->type, $this->types())) {
$this->activityObjectOutputAtom($obj, $out);
}
return true;
}
/**
* Event handler gives the plugin a chance to add custom
* JSON ActivityStreams output from a previously filled-out
* ActivityObject.
*
* The activityObjectOutputJson method is called if it's one of
* our matching types.
*
* @param ActivityObject $obj
* @param array &$out JSON-targeted array which can be modified
* @return boolean hook return value
*/
function onEndActivityObjectOutputJson(ActivityObject $obj, array &$out)
{
if (in_array($obj->type, $this->types())) {
$this->activityObjectOutputJson($obj, &$out);
}
return true;
}
function onStartShowEntryForms(&$tabs) function onStartShowEntryForms(&$tabs)
{ {
$tabs[$this->tag()] = $this->appTitle(); $tabs[$this->tag()] = $this->appTitle();

View File

@ -48,8 +48,8 @@ class PollPlugin extends MicroAppPlugin
const VERSION = '0.1'; const VERSION = '0.1';
// @fixme which domain should we use for these namespaces? // @fixme which domain should we use for these namespaces?
const POLL_OBJECT = 'http://apinamespace.org/activitystreams/object/poll'; const POLL_OBJECT = 'http://activityschema.org/object/poll';
const POLL_RESPONSE_OBJECT = 'http://apinamespace.org/activitystreams/object/poll-response'; const POLL_RESPONSE_OBJECT = 'http://activityschema.org/object/poll-response';
/** /**
* Database schema setup * Database schema setup
@ -203,26 +203,22 @@ class PollPlugin extends MicroAppPlugin
$pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll'); $pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll');
$responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response'); $responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response');
if ($pollElements->length) { if ($pollElements->length) {
$data = $pollElements->item(0); $question = '';
$question = $data->getAttribute('question');
$opts = array(); $opts = array();
foreach ($data->attributes as $node) {
$name = $node->nodeName; $data = $pollElements->item(0);
if (substr($name, 0, 6) == 'option') { foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'question') as $node) {
$n = intval(substr($name, 6)); $question = $node->textContent;
if ($n > 0) { }
$opts[$n - 1] = $node->nodeValue; foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'option') as $node) {
} $opts[] = $node->textContent;
}
} }
common_log(LOG_DEBUG, "YYY question: $question");
common_log(LOG_DEBUG, "YYY opts: " . var_export($opts, true));
try { try {
$notice = Poll::saveNew($profile, $question, $opts, $options); $notice = Poll::saveNew($profile, $question, $opts, $options);
common_log(LOG_DEBUG, "YYY ok: " . $notice->id); common_log(LOG_DEBUG, "Saved Poll from ActivityStream data ok: notice id " . $notice->id);
return $notice; return $notice;
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_DEBUG, "YYY fail: " . $e->getMessage()); common_log(LOG_DEBUG, "Poll save from ActivityStream data failed: " . $e->getMessage());
} }
} else if ($responseElements->length) { } else if ($responseElements->length) {
$data = $responseElements->item(0); $data = $responseElements->item(0);
@ -240,10 +236,10 @@ class PollPlugin extends MicroAppPlugin
} }
try { try {
$notice = Poll_response::saveNew($profile, $poll, $selection, $options); $notice = Poll_response::saveNew($profile, $poll, $selection, $options);
common_log(LOG_DEBUG, "YYY response ok: " . $notice->id); common_log(LOG_DEBUG, "Saved Poll_response ok, notice id: " . $notice->id);
return $notice; return $notice;
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_DEBUG, "YYY response fail: " . $e->getMessage()); common_log(LOG_DEBUG, "Poll response save fail: " . $e->getMessage());
} }
} else { } else {
common_log(LOG_DEBUG, "YYY no poll data"); common_log(LOG_DEBUG, "YYY no poll data");
@ -277,33 +273,15 @@ class PollPlugin extends MicroAppPlugin
$object->link = $notice->bestUrl(); $object->link = $notice->bestUrl();
$response = Poll_response::getByNotice($notice); $response = Poll_response::getByNotice($notice);
if (!$response) { if ($response) {
common_log(LOG_DEBUG, "QQQ notice uri: $notice->uri");
} else {
$poll = $response->getPoll(); $poll = $response->getPoll();
/** if ($poll) {
* For the moment, using a kind of icky-looking schema that happens to // Stash data to be formatted later by
* work with out code for generating both Atom and JSON forms, though // $this->activityObjectOutputAtom() or
* I don't like it: // $this->activityObjectOutputJson()...
* $object->pollSelection = intval($response->selection);
* <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll" $object->pollUri = $poll->uri;
* poll="http://..../poll/...." }
* selection="3" />
*
* "poll:response": {
* "xmlns:poll": http://apinamespace.org/activitystreams/object/poll
* "uri": "http://..../poll/...."
* "selection": 3
* }
*
*/
// @fixme there's no way to specify an XML node tree here, like <poll><option/><option/></poll>
// @fixme there's no way to specify a JSON array or multi-level tree unless you break the XML attribs
// @fixme XML node contents don't get shown in JSON
$data = array('xmlns:poll' => self::POLL_OBJECT,
'poll' => $poll->uri,
'selection' => intval($response->selection));
$object->extra[] = array('poll:response', $data, '');
} }
return $object; return $object;
} }
@ -318,41 +296,112 @@ class PollPlugin extends MicroAppPlugin
$object->link = $notice->bestUrl(); $object->link = $notice->bestUrl();
$poll = Poll::getByNotice($notice); $poll = Poll::getByNotice($notice);
/** if ($poll) {
* Adding the poll-specific data. There's no standard in AS for polls, // Stash data to be formatted later by
* so we're making stuff up. // $this->activityObjectOutputAtom() or
* // $this->activityObjectOutputJson()...
* For the moment, using a kind of icky-looking schema that happens to $object->pollQuestion = $poll->question;
* work with out code for generating both Atom and JSON forms, though $object->pollOptions = $poll->getOptions();
* I don't like it:
*
* <poll:data xmlns:poll="http://apinamespace.org/activitystreams/object/poll"
* question="Who wants a poll question?"
* option1="Option one"
* option2="Option two"
* option3="Option three"></poll:data>
*
* "poll:response": {
* "xmlns:poll": http://apinamespace.org/activitystreams/object/poll
* "question": "Who wants a poll question?"
* "option1": "Option one"
* "option2": "Option two"
* "option3": "Option three"
* }
*
*/
// @fixme there's no way to specify an XML node tree here, like <poll><option/><option/></poll>
// @fixme there's no way to specify a JSON array or multi-level tree unless you break the XML attribs
// @fixme XML node contents don't get shown in JSON
$data = array('xmlns:poll' => self::POLL_OBJECT,
'question' => $poll->question);
foreach ($poll->getOptions() as $i => $opt) {
$data['option' . ($i + 1)] = $opt;
} }
$object->extra[] = array('poll:poll', $data, '');
return $object; return $object;
} }
/**
* Called when generating Atom XML ActivityStreams output from an
* ActivityObject belonging to this plugin. Gives the plugin
* a chance to add custom output.
*
* Note that you can only add output of additional XML elements,
* not change existing stuff here.
*
* If output is already handled by the base Activity classes,
* you can leave this base implementation as a no-op.
*
* @param ActivityObject $obj
* @param XMLOutputter $out to add elements at end of object
*/
function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
{
if (isset($obj->pollQuestion)) {
/**
* <poll:poll xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
* <poll:question>Who wants a poll question?</poll:question>
* <poll:option>Option one</poll:option>
* <poll:option>Option two</poll:option>
* <poll:option>Option three</poll:option>
* </poll:poll>
*/
$data = array('xmlns:poll' => self::POLL_OBJECT);
$out->elementStart('poll:poll', $data);
$out->element('poll:question', array(), $obj->pollQuestion);
foreach ($obj->pollOptions as $opt) {
$out->element('poll:option', array(), $opt);
}
$out->elementEnd('poll:poll');
}
if (isset($obj->pollSelection)) {
/**
* <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
* poll="http://..../poll/...."
* selection="3" />
*/
$data = array('xmlns:poll' => self::POLL_OBJECT,
'poll' => $obj->pollUri,
'selection' => $obj->pollSelection);
$out->element('poll:response', $data, '');
}
}
/**
* Called when generating JSON ActivityStreams output from an
* ActivityObject belonging to this plugin. Gives the plugin
* a chance to add custom output.
*
* Modify the array contents to your heart's content, and it'll
* all get serialized out as JSON.
*
* If output is already handled by the base Activity classes,
* you can leave this base implementation as a no-op.
*
* @param ActivityObject $obj
* @param array &$out JSON-targeted array which can be modified
*/
public function activityObjectOutputJson(ActivityObject $obj, array &$out)
{
common_log(LOG_DEBUG, 'QQQ: ' . var_export($obj, true));
if (isset($obj->pollQuestion)) {
/**
* "poll": {
* "question": "Who wants a poll question?",
* "options": [
* "Option 1",
* "Option 2",
* "Option 3"
* ]
* }
*/
$data = array('question' => $obj->pollQuestion,
'options' => array());
foreach ($obj->pollOptions as $opt) {
$data['options'][] = $opt;
}
$out['poll'] = $data;
}
if (isset($obj->pollSelection)) {
/**
* "pollResponse": {
* "poll": "http://..../poll/....",
* "selection": 3
* }
*/
$data = array('poll' => $obj->pollUri,
'selection' => $obj->pollSelection);
$out['pollResponse'] = $data;
}
}
/** /**
* @fixme WARNING WARNING WARNING parent class closes the final div that we * @fixme WARNING WARNING WARNING parent class closes the final div that we
* open here, but we probably shouldn't open it here. Check parent class * open here, but we probably shouldn't open it here. Check parent class