X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=lib%2Factivity.php;h=2cb80f9e1a26bba80f94c19822b41f4db67fc0b1;hb=c4ee2b20bee567e1c41888bb46bfc8d5f98e8951;hp=3689dac3850a62e9a36f36b60896db0e84fc7107;hpb=3c4e9d6a2faf12637ad9b3921ea28e4d6893eea0;p=quix0rs-gnu-social.git diff --git a/lib/activity.php b/lib/activity.php index 3689dac385..2cb80f9e1a 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -34,6 +34,7 @@ if (!defined('STATUSNET')) { class PoCoURL { + const URLS = 'urls'; const TYPE = 'type'; const VALUE = 'value'; const PRIMARY = 'primary'; @@ -55,7 +56,7 @@ class PoCoURL $xs->elementStart('poco:urls'); $xs->element('poco:type', null, $this->type); $xs->element('poco:value', null, $this->value); - if ($this->primary) { + if (!empty($this->primary)) { $xs->element('poco:primary', null, 'true'); } $xs->elementEnd('poco:urls'); @@ -70,21 +71,19 @@ class PoCoAddress public $formatted; - function __construct($formatted) - { - if (empty($formatted)) { - return null; - } - $this->formatted = $formatted; - } + // @todo Other address fields function asString() { - $xs = new XMLStringer(true); - $xs->elementStart('poco:address'); - $xs->element('poco:formatted', null, $this->formatted); - $xs->elementEnd('poco:address'); - return $xs->getString(); + if (!empty($this->formatted)) { + $xs = new XMLStringer(true); + $xs->elementStart('poco:address'); + $xs->element('poco:formatted', null, $this->formatted); + $xs->elementEnd('poco:address'); + return $xs->getString(); + } + + return null; } } @@ -92,25 +91,127 @@ class PoCo { const NS = 'http://portablecontacts.net/spec/1.0'; - const USERNAME = 'preferredUsername'; - const NOTE = 'note'; - const URLS = 'urls'; + const USERNAME = 'preferredUsername'; + const DISPLAYNAME = 'displayName'; + const NOTE = 'note'; public $preferredUsername; + public $displayName; public $note; public $address; public $urls = array(); - function __construct($profile) + function __construct($element = null) + { + if (empty($element)) { + return; + } + + $this->preferredUsername = ActivityUtils::childContent( + $element, + self::USERNAME, + self::NS + ); + + $this->displayName = ActivityUtils::childContent( + $element, + self::DISPLAYNAME, + self::NS + ); + + $this->note = ActivityUtils::childContent( + $element, + self::NOTE, + self::NS + ); + + $this->address = $this->_getAddress($element); + $this->urls = $this->_getURLs($element); + } + + private function _getURLs($element) + { + $urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS); + $urls = array(); + + foreach ($urlEls as $urlEl) { + + $type = ActivityUtils::childContent( + $urlEl, + PoCoURL::TYPE, + PoCo::NS + ); + + $value = ActivityUtils::childContent( + $urlEl, + PoCoURL::VALUE, + PoCo::NS + ); + + $primary = ActivityUtils::childContent( + $urlEl, + PoCoURL::PRIMARY, + PoCo::NS + ); + + $isPrimary = false; + + if (isset($primary) && $primary == 'true') { + $isPrimary = true; + } + + // @todo check to make sure a primary hasn't already been added + + array_push($urls, new PoCoURL($type, $value, $isPrimary)); + } + return $urls; + } + + private function _getAddress($element) + { + $addressEl = ActivityUtils::child( + $element, + PoCoAddress::ADDRESS, + PoCo::NS + ); + + if (!empty($addressEl)) { + $formatted = ActivityUtils::childContent( + $addressEl, + PoCoAddress::FORMATTED, + self::NS + ); + + if (!empty($formatted)) { + $address = new PoCoAddress(); + $address->formatted = $formatted; + return $address; + } + } + + return null; + } + + function fromProfile($profile) { - $this->preferredUsername = $profile->nickname; + if (empty($profile)) { + return null; + } + + $poco = new PoCo(); + + $poco->preferredUsername = $profile->nickname; + $poco->displayName = $profile->getBestName(); - $this->note = $profile->bio; - $this->address = new PoCoAddress($profile->location); + $poco->note = $profile->bio; + + $paddy = new PoCoAddress(); + $paddy->formatted = $profile->location; + $poco->address = $paddy; if (!empty($profile->homepage)) { array_push( - $this->urls, + $poco->urls, new PoCoURL( 'homepage', $profile->homepage, @@ -118,6 +219,48 @@ class PoCo ) ); } + + return $poco; + } + + function fromGroup($group) + { + if (empty($group)) { + return null; + } + + $poco = new PoCo(); + + $poco->preferredUsername = $group->nickname; + $poco->displayName = $group->getBestName(); + + $poco->note = $group->description; + + $paddy = new PoCoAddress(); + $paddy->formatted = $group->location; + $poco->address = $paddy; + + if (!empty($group->homepage)) { + array_push( + $poco->urls, + new PoCoURL( + 'homepage', + $group->homepage, + true + ) + ); + } + + return $poco; + } + + function getPrimaryURL() + { + foreach ($this->urls as $url) { + if ($url->primary) { + return $url; + } + } } function asString() @@ -129,6 +272,12 @@ class PoCo $this->preferredUsername ); + $xs->element( + 'poco:displayName', + null, + $this->displayName + ); + if (!empty($this->note)) { $xs->element('poco:note', null, $this->note); } @@ -193,24 +342,47 @@ class ActivityUtils * @return string related link, if any */ - static function getLink($element, $rel, $type=null) + static function getLink(DOMNode $element, $rel, $type=null) { - $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + $els = $element->childNodes; - foreach ($links as $link) { + foreach ($els as $link) { + if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { - $linkRel = $link->getAttribute(self::REL); - $linkType = $link->getAttribute(self::TYPE); + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); - if ($linkRel == $rel && - (is_null($type) || $linkType == $type)) { - return $link->getAttribute(self::HREF); + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + return $link->getAttribute(self::HREF); + } } } return null; } + static function getLinks(DOMNode $element, $rel, $type=null) + { + $els = $element->childNodes; + $out = array(); + + foreach ($els as $link) { + if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + $out[] = $link; + } + } + } + + return $out; + } + /** * Gets the first child element with the given tag * @@ -221,7 +393,7 @@ class ActivityUtils * @return DOMElement found element or null */ - static function child($element, $tag, $namespace=self::ATOM) + static function child(DOMNode $element, $tag, $namespace=self::ATOM) { $els = $element->childNodes; if (empty($els) || $els->length == 0) { @@ -246,7 +418,7 @@ class ActivityUtils * @return string content of the child */ - static function childContent($element, $tag, $namespace=self::ATOM) + static function childContent(DOMNode $element, $tag, $namespace=self::ATOM) { $el = self::child($element, $tag, $namespace); @@ -316,6 +488,75 @@ class ActivityUtils } } +// XXX: Arg! This wouldn't be necessary if we used Avatars conistently +class AvatarLink +{ + public $url; + public $type; + public $size; + public $width; + public $height; + + function __construct($element=null) + { + if ($element) { + // @fixme use correct namespaces + $this->url = $element->getAttribute('href'); + $this->type = $element->getAttribute('type'); + $width = $element->getAttribute('media:width'); + if ($width != null) { + $this->width = intval($width); + } + $height = $element->getAttribute('media:height'); + if ($height != null) { + $this->height = intval($height); + } + } + } + + static function fromAvatar($avatar) + { + if (empty($avatar)) { + return null; + } + $alink = new AvatarLink(); + $alink->type = $avatar->mediatype; + $alink->height = $avatar->height; + $alink->width = $avatar->width; + $alink->url = $avatar->displayUrl(); + return $alink; + } + + static function fromFilename($filename, $size) + { + $alink = new AvatarLink(); + $alink->url = $filename; + $alink->height = $size; + if (!empty($filename)) { + $alink->width = $size; + $alink->type = self::mediatype($filename); + } else { + $alink->url = User_group::defaultLogo($size); + $alink->type = 'image/png'; + } + return $alink; + } + + // yuck! + static function mediatype($filename) { + $ext = strtolower(end(explode('.', $filename))); + if ($ext == 'jpeg') { + $ext = 'jpg'; + } + // hope we don't support any others + $types = array('png', 'gif', 'jpg', 'jpeg'); + if (in_array($ext, $types)) { + return 'image/' . $ext; + } + return null; + } +} + /** * A noun-ish thing in the activity universe * @@ -372,8 +613,10 @@ class ActivityObject public $content; public $link; public $source; - public $avatar; + public $avatarLinks = array(); public $geopoint; + public $poco; + public $displayName; /** * Constructor @@ -393,6 +636,12 @@ class ActivityObject $this->element = $element; + $this->geopoint = $this->_childContent( + $element, + ActivityContext::POINT, + ActivityContext::GEORSS + ); + if ($element->tagName == 'author') { $this->type = self::PERSON; // XXX: is this fair? @@ -426,16 +675,18 @@ class ActivityObject $this->link = ActivityUtils::getPermalink($element); - // XXX: grab PoCo stuff } // Some per-type attributes... if ($this->type == self::PERSON || $this->type == self::GROUP) { $this->displayName = $this->title; - // @fixme we may have multiple avatars with different resolutions specified - $this->avatar = ActivityUtils::getLink($element, 'avatar'); - $this->nickname = ActivityUtils::childContent($element, PoCo::USERNAME, PoCo::NS); + $avatars = ActivityUtils::getLinks($element, 'avatar'); + foreach ($avatars as $link) { + $this->avatarLinks[] = new AvatarLink($link); + } + + $this->poco = new PoCo($element); } } @@ -484,17 +735,77 @@ class ActivityObject $object->id = $profile->getUri(); $object->title = $profile->getBestName(); $object->link = $profile->profileurl; - $object->avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + + $orig = $profile->getOriginalAvatar(); + + if (!empty($orig)) { + $object->avatarLinks[] = AvatarLink::fromAvatar($orig); + } + + $sizes = array( + AVATAR_PROFILE_SIZE, + AVATAR_STREAM_SIZE, + AVATAR_MINI_SIZE + ); + + foreach ($sizes as $size) { + + $alink = null; + $avatar = $profile->getAvatar($size); + + if (!empty($avatar)) { + $alink = AvatarLink::fromAvatar($avatar); + } else { + $alink = new AvatarLink(); + $alink->type = 'image/png'; + $alink->height = $size; + $alink->width = $size; + $alink->url = Avatar::defaultImage($size); + } + + $object->avatarLinks[] = $alink; + } if (isset($profile->lat) && isset($profile->lon)) { - $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; + $object->geopoint = (float)$profile->lat + . ' ' . (float)$profile->lon; } - $object->poco = new PoCo($profile); + $object->poco = PoCo::fromProfile($profile); + + return $object; + } + + static function fromGroup($group) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::GROUP; + $object->id = $group->getUri(); + $object->title = $group->getBestName(); + $object->link = $group->getUri(); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->homepage_logo, + AVATAR_PROFILE_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->stream_logo, + AVATAR_STREAM_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->mini_logo, + AVATAR_MINI_SIZE + ); + + $object->poco = PoCo::fromGroup($group); return $object; } + function asString($tag='activity:object') { $xs = new XMLStringer(true); @@ -519,23 +830,34 @@ class ActivityObject } if (!empty($this->link)) { - $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), - $this->link); - } - - if ($this->type == ActivityObject::PERSON) { $xs->element( - 'link', array( - 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($this->avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $this->avatar->displayUrl() + '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) { + $xs->element( + 'link', array( + 'rel' => 'avatar', + 'type' => $avatar->type, + 'media:width' => $avatar->width, + 'media:height' => $avatar->height, + 'href' => $avatar->url + ), + null + ); + } + } + if (!empty($this->geopoint)) { $xs->element( 'georss:point', @@ -582,6 +904,9 @@ class ActivityVerb const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite'; const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow'; const LEAVE = 'http://ostatus.org/schema/1.0/leave'; + + // For simple profile-update pings; no content to share. + const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile'; } class ActivityContext @@ -647,22 +972,29 @@ class ActivityContext for ($i = 0; $i < $points->length; $i++) { $point = $points->item($i)->textContent; - $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" - $point = preg_replace('/\s+/', ' ', $point); - $point = trim($point); - $coords = explode(' ', $point); - if (count($coords) == 2) { - list($lat, $lon) = $coords; - if (is_numeric($lat) && is_numeric($lon)) { - common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); - return Location::fromLatLon($lat, $lon); - } - } - common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + return self::locationFromPoint($point); } return null; } + + // XXX: Move to ActivityUtils or Location? + static function locationFromPoint($point) + { + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); + $coords = explode(' ', $point); + if (count($coords) == 2) { + list($lat, $lon) = $coords; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss point"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + return null; + } } /** @@ -715,6 +1047,8 @@ class Activity public $content; // HTML content of activity public $id; // ID of the activity public $title; // title of the activity + public $categories = array(); // list of AtomCategory objects + public $enclosures = array(); // list of enclosure URL references /** * Turns a regular old Atom into a magical activity @@ -730,6 +1064,18 @@ class Activity } $this->entry = $entry; + + // @fixme Don't send in a DOMDocument + if ($feed instanceof DOMDocument) { + common_log( + LOG_WARNING, + 'Activity::__construct() - ' + . 'DOMDocument passed in for feed by mistake. ' + . "Expecting a 'feed' DOMElement." + ); + $feed = $feed->getElementsByTagName('feed')->item(0); + } + $this->feed = $feed; $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); @@ -803,6 +1149,18 @@ class Activity $this->summary = ActivityUtils::childContent($entry, 'summary'); $this->id = ActivityUtils::childContent($entry, 'id'); $this->content = ActivityUtils::getContent($entry); + + $catEls = $entry->getElementsByTagNameNS(self::ATOM, 'category'); + if ($catEls) { + for ($i = 0; $i < $catEls->length; $i++) { + $catEl = $catEls->item($i); + $this->categories[] = new AtomCategory($catEl); + } + } + + foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) { + $this->enclosures[] = $link->getAttribute('href'); + } } /** @@ -823,7 +1181,10 @@ class Activity if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:ostatus' => 'http://ostatus.org/schema/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'); } else { $attrs = array(); } @@ -846,11 +1207,28 @@ class Activity } // XXX: add context - // XXX: add target + $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); - $xs->raw($this->object->asString()); + + if ($this->object) { + $xs->raw($this->object->asString()); + } + + if ($this->target) { + $xs->raw($this->target->asString('activity:target')); + } + + foreach ($this->categories as $cat) { + $xs->raw($cat->asString()); + } $xs->elementEnd('entry'); @@ -861,4 +1239,50 @@ class Activity { return ActivityUtils::child($element, $tag, $namespace); } -} \ No newline at end of file +} + +class AtomCategory +{ + public $term; + public $scheme; + public $label; + + function __construct($element=null) + { + if ($element && $element->attributes) { + $this->term = $this->extract($element, 'term'); + $this->scheme = $this->extract($element, 'scheme'); + $this->label = $this->extract($element, 'label'); + } + } + + protected function extract($element, $attrib) + { + $node = $element->attributes->getNamedItemNS(Activity::ATOM, $attrib); + if ($node) { + return trim($node->textContent); + } + $node = $element->attributes->getNamedItem($attrib); + if ($node) { + return trim($node->textContent); + } + return null; + } + + function asString() + { + $attribs = array(); + if ($this->term !== null) { + $attribs['term'] = $this->term; + } + if ($this->scheme !== null) { + $attribs['scheme'] = $this->scheme; + } + if ($this->label !== null) { + $attribs['label'] = $this->label; + } + $xs = new XMLStringer(); + $xs->element('category', $attribs); + return $xs->asString(); + } +}