X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=lib%2Factivity.php;h=b781e498467f0331dbf9dd10db41d015b0cf1faa;hb=805d14577d8a7e87d2dcea66aa5d22d801987fa5;hp=8e2da99bb3afe46cb559a97178bc9e970178b86d;hpb=696e4ba393c658d5b2e1fe46e1389bd7b2cfdb34;p=quix0rs-gnu-social.git diff --git a/lib/activity.php b/lib/activity.php index 8e2da99bb3..b781e49846 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -48,7 +48,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 * @link http://status.net/ */ - class Activity { const SPEC = 'http://activitystrea.ms/spec/1.0/'; @@ -102,13 +101,17 @@ class Activity public $categories = array(); // list of AtomCategory objects public $enclosures = array(); // list of enclosure URL references + public $extra = array(); // extra elements as array(tag, attrs, content) + public $source; // ActivitySource object representing 'home feed' + public $selfLink; // + public $editLink; // + /** * Turns a regular old Atom into a magical activity * * @param DOMElement $entry Atom entry to poke at * @param DOMElement $feed Atom feed, for context */ - function __construct($entry = null, $feed = null) { if (is_null($entry)) { @@ -133,6 +136,7 @@ class Activity $entry->localName == 'item') { $this->_fromRssItem($entry, $feed); } else { + // Low level exception. No need for i18n. throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}"); } } @@ -178,6 +182,9 @@ class Activity $actorEl = $this->_child($entry, self::ACTOR); if (!empty($actorEl)) { + // Standalone elements are a holdover from older + // versions of ActivityStreams. Newer feeds should have this data + // integrated straight into . $this->actor = new ActivityObject($actorEl); @@ -192,18 +199,24 @@ class Activity $this->actor->id = $authorObj->id; } } + } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { + + // An in the entry overrides any author info on + // the surrounding feed. + $this->actor = new ActivityObject($authorEl); + } else if (!empty($feed) && $subjectEl = $this->_child($feed, self::SUBJECT)) { + // Feed subject is used for things like groups. + // Should actually possibly not be interpreted as an actor...? $this->actor = new ActivityObject($subjectEl); - } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { - - $this->actor = new ActivityObject($authorEl); - } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, self::ATOM)) { + // If there's no on the entry, it's safe to assume + // the containing feed's authorship info applies. $this->actor = new ActivityObject($authorEl); } @@ -236,6 +249,11 @@ class Activity foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) { $this->enclosures[] = $link->getAttribute('href'); } + + // From APP. Might be useful. + + $this->selfLink = ActivityUtils::getLink($entry, 'self', 'application/atom+xml'); + $this->editLink = ActivityUtils::getLink($entry, 'edit', 'application/atom+xml'); } function _fromRssItem($item, $channel) @@ -319,72 +337,378 @@ class Activity return null; } - function asString($namespace=false) + /** + * 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); + $this->outputTo($xs, $namespace, $author, $source); + return $xs->getString(); + } + function outputTo($xs, $namespace=false, $author=true, $source=false) + { if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', 'xmlns:georss' => 'http://www.georss.org/georss', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', - 'xmlns:media' => 'http://purl.org/syndication/atommedia'); + 'xmlns:media' => 'http://purl.org/syndication/atommedia', + 'xmlns:statusnet' => 'http://status.net/schema/api/1/'); } else { $attrs = array(); } $xs->elementStart('entry', $attrs); - $xs->element('id', null, $this->id); - $xs->element('title', null, $this->title); - $xs->element('published', null, common_date_iso8601($this->time)); - $xs->element('content', array('type' => 'html'), $this->content); + if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { - if (!empty($this->summary)) { - $xs->element('summary', null, $this->summary); - } + $obj = $this->objects[0]; + $obj->outputTo($xs, null); - if (!empty($this->link)) { - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html'), - $this->link); - } + } else { + $xs->element('id', null, $this->id); + $xs->element('title', null, $this->title); - // XXX: add context + $xs->element('content', array('type' => 'html'), $this->content); + + if (!empty($this->summary)) { + $xs->element('summary', null, $this->summary); + } + + if (!empty($this->link)) { + $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html'), + $this->link); + } - $xs->elementStart('author'); - $xs->element('uri', array(), $this->actor->id); - if ($this->actor->title) { - $xs->element('name', array(), $this->actor->title); } - $xs->elementEnd('author'); - $xs->raw($this->actor->asString('activity:actor')); $xs->element('activity:verb', null, $this->verb); - if (!empty($this->objects)) { + $published = self::iso8601Date($this->time); + + $xs->element('published', null, $published); + $xs->element('updated', null, $published); + + if ($author) { + $this->actor->outputTo($xs, 'author'); + + // XXX: Remove ASAP! Author information + // has been moved to the author element in the Activity + // Streams spec. We're outputting actor only for backward + // compatibility with clients that can only parse + // activities based on older versions of the spec. + + $depMsg = 'Deprecation warning: activity:actor is present ' + . 'only for backward compatibility. It will be ' + . 'removed in the next version of StatusNet.'; + $xs->comment($depMsg); + $this->actor->outputTo($xs, 'activity:actor'); + } + + if ($this->verb != ActivityVerb::POST || count($this->objects) != 1) { foreach($this->objects as $object) { - $xs->raw($object->asString()); + $object->outputTo($xs, 'activity:object'); + } + } + + if (!empty($this->context)) { + + if (!empty($this->context->replyToID)) { + if (!empty($this->context->replyToUrl)) { + $xs->element('thr:in-reply-to', + array('ref' => $this->context->replyToID, + 'href' => $this->context->replyToUrl)); + } else { + $xs->element('thr:in-reply-to', + array('ref' => $this->context->replyToID)); + } + } + + if (!empty($this->context->replyToUrl)) { + $xs->element('link', array('rel' => 'related', + 'href' => $this->context->replyToUrl)); + } + + if (!empty($this->context->conversation)) { + $xs->element('link', array('rel' => 'ostatus:conversation', + 'href' => $this->context->conversation)); + } + + foreach ($this->context->attention as $attnURI) { + $xs->element('link', array('rel' => 'ostatus:attention', + 'href' => $attnURI)); + $xs->element('link', array('rel' => 'mentioned', + 'href' => $attnURI)); + } + + // XXX: shoulda used ActivityVerb::SHARE + + if (!empty($this->context->forwardID)) { + if (!empty($this->context->forwardUrl)) { + $xs->element('ostatus:forward', + array('ref' => $this->context->forwardID, + 'href' => $this->context->forwardUrl)); + } else { + $xs->element('ostatus:forward', + array('ref' => $this->context->forwardID)); + } + } + + if (!empty($this->context->location)) { + $loc = $this->context->location; + $xs->element('georss:point', null, $loc->lat . ' ' . $loc->lon); } } if ($this->target) { - $xs->raw($this->target->asString('activity:target')); + $this->target->outputTo($xs, 'activity:target'); } foreach ($this->categories as $cat) { - $xs->raw($cat->asString()); + $cat->outputTo($xs); + } + + // can be either URLs or enclosure objects + + foreach ($this->enclosures as $enclosure) { + if (is_string($enclosure)) { + $xs->element('link', array('rel' => 'enclosure', + 'href' => $enclosure)); + } else { + $attributes = array('rel' => 'enclosure', + 'href' => $enclosure->url, + 'type' => $enclosure->mimetype, + 'length' => $enclosure->size); + if ($enclosure->title) { + $attributes['title'] = $enclosure->title; + } + $xs->element('link', $attributes); + } + } + + // Info on the source feed + + if ($source && !empty($this->source)) { + $xs->elementStart('source'); + + $xs->element('id', null, $this->source->id); + $xs->element('title', null, $this->source->title); + + if (array_key_exists('alternate', $this->source->links)) { + $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html', + 'href' => $this->source->links['alternate'])); + } + + if (array_key_exists('self', $this->source->links)) { + $xs->element('link', array('rel' => 'self', + 'type' => 'application/atom+xml', + 'href' => $this->source->links['self'])); + } + + if (array_key_exists('license', $this->source->links)) { + $xs->element('link', array('rel' => 'license', + 'href' => $this->source->links['license'])); + } + + if (!empty($this->source->icon)) { + $xs->element('icon', null, $this->source->icon); + } + + if (!empty($this->source->updated)) { + $xs->element('updated', null, $this->source->updated); + } + + $xs->elementEnd('source'); + } + + if (!empty($this->selfLink)) { + $xs->element('link', array('rel' => 'self', + 'type' => 'application/atom+xml', + 'href' => $this->selfLink)); + } + + if (!empty($this->editLink)) { + $xs->element('link', array('rel' => 'edit', + 'type' => 'application/atom+xml', + 'href' => $this->editLink)); + } + + // For throwing in extra elements; used for statusnet:notice_info + + foreach ($this->extra as $el) { + list($tag, $attrs, $content) = $el; + $xs->element($tag, $attrs, $content); } $xs->elementEnd('entry'); - return $xs->getString(); + return; } private function _child($element, $tag, $namespace=self::SPEC) { return ActivityUtils::child($element, $tag, $namespace); } -} + static function iso8601Date($tm) + { + $dateStr = date('d F Y H:i:s', $tm); + $d = new DateTime($dateStr, new DateTimeZone('UTC')); + $d->setTimezone(new DateTimeZone(common_timezone())); + return $d->format('c'); + } +}