X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=lib%2Factivity.php;h=3128088ca21b45772ff844e60ce98cc14378888d;hb=77951647cef2b025f4c5731c00713a455d20498d;hp=b781e498467f0331dbf9dd10db41d015b0cf1faa;hpb=27ef3b1d905cdf8f47b47293a757624dda88fdc7;p=quix0rs-gnu-social.git diff --git a/lib/activity.php b/lib/activity.php index b781e49846..3128088ca2 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -100,12 +100,13 @@ class Activity public $title; // title of the activity public $categories = array(); // list of AtomCategory objects public $enclosures = array(); // list of enclosure URL references + public $attachments = array(); // list of attachments public $extra = array(); // extra elements as array(tag, attrs, content) public $source; // ActivitySource object representing 'home feed' public $selfLink; // public $editLink; // - + public $generator; // ActivityObject representing the generating application /** * Turns a regular old Atom into a magical activity * @@ -135,6 +136,9 @@ class Activity } else if ($entry->namespaceURI == Activity::RSS && $entry->localName == 'item') { $this->_fromRssItem($entry, $feed); + } else if ($entry->namespaceURI == Activity::SPEC && + $entry->localName == 'object') { + $this->_fromAtomEntry($entry, $feed); } else { // Low level exception. No need for i18n. throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}"); @@ -168,14 +172,22 @@ class Activity // XXX: do other implied stuff here } - $objectEls = $entry->getElementsByTagNameNS(self::SPEC, self::OBJECT); + // get immediate object children - if ($objectEls->length > 0) { - for ($i = 0; $i < $objectEls->length; $i++) { - $objectEl = $objectEls->item($i); - $this->objects[] = new ActivityObject($objectEl); + $objectEls = ActivityUtils::children($entry, self::OBJECT, self::SPEC); + + if (count($objectEls) > 0) { + foreach ($objectEls as $objectEl) { + // Special case for embedded activities + $objectType = ActivityUtils::childContent($objectEl, self::OBJECTTYPE, self::SPEC); + if (!empty($objectType) && $objectType == ActivityObject::ACTIVITY) { + $this->objects[] = new Activity($objectEl); + } else { + $this->objects[] = new ActivityObject($objectEl); + } } } else { + // XXX: really? $this->objects[] = new ActivityObject($entry); } @@ -232,6 +244,9 @@ class Activity if (!empty($targetEl)) { $this->target = new ActivityObject($targetEl); + } elseif (ActivityUtils::compareVerbs($this->verb, array(ActivityVerb::FAVORITE))) { + // StatusNet didn't send a 'target' for their Favorite atom entries + $this->target = clone($this->objects[0]); } $this->summary = ActivityUtils::childContent($entry, 'summary'); @@ -351,91 +366,100 @@ class Activity // 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. + // content + $activity['content'] = $this->content; - // icon <-- I've decided to use the posting user's stream avatar here - // for now (also included in the avatarLinks extension) + // generator + if (!empty($this->generator)) { + $activity['generator'] = $this->generator->asArray(); + } - // object - if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { - $activity['object'] = $this->objects[0]->asArray(); + // icon <-- possibly a mini object representing verb? - // Context stuff. For now I'm just sticking most of it - // in a property called "context" + // id + $activity['id'] = $this->id; - if (!empty($this->context)) { + // object - if (!empty($this->context->location)) { - $loc = $this->context->location; + if (count($this->objects) == 0) { + common_log(LOG_ERR, "Can't save " . $this->id); + } else { + if (count($this->objects) > 1) { + common_log(LOG_WARNING, "Ignoring " . (count($this->objects) - 1) . " extra objects in JSON output for activity " . $this->id); + } + $object = $this->objects[0]; + + if ($object instanceof Activity) { + // Sharing a post activity is more like sharing the original object + if (ActivityVerb::canonical($this->verb) == ActivityVerb::canonical(ActivityVerb::SHARE) && + ActivityVerb::canonical($object->verb) == ActivityVerb::canonical(ActivityVerb::POST)) { + // XXX: Here's one for the obfuscation record books + $object = $object->objects[0]; + } + } - // GeoJSON + $activity['object'] = $object->asArray(); - $activity['geopoint'] = array( - 'type' => 'Point', - 'coordinates' => array($loc->lat, $loc->lon) - ); + if ($object instanceof Activity) { + $activity['object']['objectType'] = 'activity'; + } + foreach ($this->attachments as $attachment) { + if (empty($activity['object']['attachments'])) { + $activity['object']['attachments'] = array(); } - - $activity['to'] = $this->context->getToArray(); - $activity['context'] = $this->context->asArray(); + $activity['object']['attachments'][] = $attachment->asArray(); } + } + + // Context stuff. - // 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. + if (!empty($this->context)) { - foreach ($this->enclosures as $enclosure) { + if (!empty($this->context->location)) { + $loc = $this->context->location; - if (is_string($enclosure)) { + $activity['location'] = array( + 'objectType' => 'place', + 'position' => sprintf("%+02.5F%+03.5F/", $loc->lat, $loc->lon), + 'lat' => $loc->lat, + 'lon' => $loc->lon + ); - $attachedObjects[]['id'] = $enclosure; + $name = $loc->getName(); - } else { + if ($name) { + $activity['location']['displayName'] = $name; + } + + $url = $loc->getURL(); - $attachedObjects[]['id'] = $enclosure->url; + if ($url) { + $activity['location']['url'] = $url; + } + } - $mediaLink = new ActivityStreamsMediaLink( - $enclosure->url, - null, - null, - $enclosure->mimetype - // XXX: Add 'size' as an extension to MediaLink? - ); + $activity['to'] = $this->context->getToArray(); - $attachedObjects[]['mediaLink'] = $mediaLink->asArray(); // extension + $ctxarr = $this->context->asArray(); - if ($enclosure->title) { - $attachedObjects[]['displayName'] = $enclosure->title; - } - } + if (array_key_exists('inReplyTo', $ctxarr)) { + $activity['object']['inReplyTo'] = $ctxarr['inReplyTo']; + unset($ctxarr['inReplyTo']); } - if (!empty($attachedObjects)) { - $activity['object']['attachedObjects'] = $attachedObjects; + if (!array_key_exists('status_net', $activity)) { + $activity['status_net'] = array(); } - } else { - $activity['object'] = array(); - foreach($this->objects as $object) { - $activity['object'][] = $object->asArray(); + foreach ($ctxarr as $key => $value) { + $activity['status_net'][$key] = $value; } } - $activity['postedTime'] = self::iso8601Date($this->time); // Change to exactly be RFC3339? + // published + $activity['published'] = self::iso8601Date($this->time); // provider $provider = array( @@ -454,25 +478,33 @@ class Activity // title $activity['title'] = $this->title; - // updatedTime <-- Should we use this to indicate the time we received - // a remote notice? Probably not. + // updated <-- Optional. Should we use this to indicate the time we r + // eceived 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(); + $activity['verb'] = ActivityVerb::canonical($this->verb); - // Use an Activity Object for term? Which object? Note? - foreach ($this->categories as $cat) { - $tags[] = $cat->term; + // url + if ($this->link) { + $activity['url'] = $this->link; } - $activity['tags'] = $tags; + /* Purely extensions hereafter */ + + if ($activity['verb'] == 'post') { + $tags = array(); + foreach ($this->categories as $cat) { + if (mb_strlen($cat->term) > 0) { + // Couldn't figure out which object type to use, so... + $tags[] = array('objectType' => 'http://activityschema.org/object/hashtag', + 'displayName' => $cat->term); + } + } + if (count($tags) > 0) { + $activity['object']['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 @@ -481,7 +513,15 @@ class Activity foreach ($this->extra as $e) { list($objectName, $props, $txt) = $e; if (!empty($objectName)) { - $activity[$objectName] = $props; + $parts = explode(":", $objectName); + if (count($parts) == 2 && $parts[0] == "statusnet") { + if (!array_key_exists('status_net', $activity)) { + $activity['status_net'] = array(); + } + $activity['status_net'][$parts[1]] = $props; + } else { + $activity[$objectName] = $props; + } } } @@ -495,7 +535,7 @@ class Activity return $xs->getString(); } - function outputTo($xs, $namespace=false, $author=true, $source=false) + function outputTo($xs, $namespace=false, $author=true, $source=false, $tag='entry') { if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', @@ -510,16 +550,26 @@ class Activity $attrs = array(); } - $xs->elementStart('entry', $attrs); + $xs->elementStart($tag, $attrs); + + if ($tag != 'entry') { + $xs->element('activity:object-type', null, ActivityObject::ACTIVITY); + } - if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) { + if ($this->verb == ActivityVerb::POST && count($this->objects) == 1 && $tag == 'entry') { $obj = $this->objects[0]; $obj->outputTo($xs, null); } else { $xs->element('id', null, $this->id); - $xs->element('title', null, $this->title); + + if ($this->title) { + $xs->element('title', null, $this->title); + } else { + // Require element + $xs->element('title', null, ""); + } $xs->element('content', array('type' => 'html'), $this->content); @@ -529,8 +579,8 @@ class Activity if (!empty($this->link)) { $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html'), - $this->link); + 'type' => 'text/html', + 'href' => $this->link)); } } @@ -544,23 +594,15 @@ class Activity 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) { + if ($this->verb != ActivityVerb::POST || count($this->objects) != 1 || $tag != 'entry') { foreach($this->objects as $object) { - $object->outputTo($xs, 'activity:object'); + if ($object instanceof Activity) { + $object->outputTo($xs, false, true, true, 'activity:object'); + } else { + $object->outputTo($xs, 'activity:object'); + } } } @@ -583,30 +625,24 @@ class Activity } if (!empty($this->context->conversation)) { - $xs->element('link', array('rel' => 'ostatus:conversation', + $xs->element('link', array('rel' => ActivityContext::CONVERSATION, 'href' => $this->context->conversation)); + $xs->element(ActivityContext::CONVERSATION, null, $this->context->conversation); + /* Since we use XMLWriter we just use the previously hardcoded prefix for ostatus, + otherwise we should use something like this: + $xs->elementNS(array(ActivityContext::OSTATUS => 'ostatus'), // namespace + 'conversation', // tag (or the element name from ActivityContext::CONVERSATION) + null, // attributes + $this->context->conversation); // content + */ } - foreach ($this->context->attention as $attnURI) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $attnURI)); - $xs->element('link', array('rel' => 'mentioned', + foreach ($this->context->attention as $attnURI=>$type) { + $xs->element('link', array('rel' => ActivityContext::MENTIONED, + ActivityContext::OBJECTTYPE => $type, // FIXME: undocumented '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); @@ -694,7 +730,7 @@ class Activity $xs->element($tag, $attrs, $content); } - $xs->elementEnd('entry'); + $xs->elementEnd($tag); return; } @@ -704,11 +740,18 @@ class Activity return ActivityUtils::child($element, $tag, $namespace); } + /** + * For consistency, we'll always output UTC rather than local time. + * Note that clients *should* accept any timezone we give them as long + * as it's properly formatted. + * + * @param int $tm Unix timestamp + * @return string + */ 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'); } }