]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - lib/activity.php
throw an error that looks like mysql errors.. :-S
[quix0rs-gnu-social.git] / lib / activity.php
index 04c57c561c53f95db2a54fbf25c14233ce593929..2cb80f9e1a26bba80f94c19822b41f4db67fc0b1 100644 (file)
@@ -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 <entry> 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');
+        }
     }
 
     /**
@@ -825,7 +1183,8 @@ class Activity
                            '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:poco' => 'http://portablecontacts.net/spec/1.0',
+                           'xmlns:media' => 'http://purl.org/syndication/atommedia');
         } else {
             $attrs = array();
         }
@@ -848,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');
 
@@ -863,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();
+    }
+}