]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - lib/activity.php
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into 0.9.x
[quix0rs-gnu-social.git] / lib / activity.php
index 5cbab8d5f353296101c953a0baf4d14740b9b341..9ffd822db994c597a904c0f5609d21fe933e9801 100644 (file)
@@ -78,7 +78,7 @@ class PoCoAddress
         if (!empty($this->formatted)) {
             $xs = new XMLStringer(true);
             $xs->elementStart('poco:address');
-            $xs->element('poco:formatted', null, $this->formatted);
+            $xs->element('poco:formatted', null, common_xml_safe_str($this->formatted));
             $xs->elementEnd('poco:address');
             return $xs->getString();
         }
@@ -154,7 +154,15 @@ class PoCo
                 PoCo::NS
             );
 
-            array_push($urls, new PoCoURL($type, $value, $primary));
+            $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;
     }
@@ -167,16 +175,18 @@ class PoCo
             PoCo::NS
         );
 
-        $formatted = ActivityUtils::childContent(
-            $addressEl,
-            PoCoAddress::FORMATTED,
-            self::NS
-        );
+        if (!empty($addressEl)) {
+            $formatted = ActivityUtils::childContent(
+                $addressEl,
+                PoCoAddress::FORMATTED,
+                self::NS
+            );
 
-        if (!empty($formatted)) {
-            $address = new PoCoAddress();
-            $address->formatted = $formatted;
-            return $address;
+            if (!empty($formatted)) {
+                $address = new PoCoAddress();
+                $address->formatted = $formatted;
+                return $address;
+            }
         }
 
         return null;
@@ -213,6 +223,46 @@ 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()
     {
         $xs = new XMLStringer(true);
@@ -229,7 +279,7 @@ class PoCo
         );
 
         if (!empty($this->note)) {
-            $xs->element('poco:note', null, $this->note);
+            $xs->element('poco:note', null, common_xml_safe_str($this->note));
         }
 
         if (!empty($this->address)) {
@@ -292,24 +342,52 @@ 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 instanceof DOMElement)) {
+                continue;
+            }
 
-            $linkRel = $link->getAttribute(self::REL);
-            $linkType = $link->getAttribute(self::TYPE);
+            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
 
-            if ($linkRel == $rel &&
-                (is_null($type) || $linkType == $type)) {
-                return $link->getAttribute(self::HREF);
+                $linkRel = $link->getAttribute(self::REL);
+                $linkType = $link->getAttribute(self::TYPE);
+
+                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
      *
@@ -320,7 +398,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) {
@@ -345,7 +423,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);
 
@@ -384,13 +462,16 @@ class ActivityUtils
 
             // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
 
-            if ($type == 'text') {
-                return $contentEl->textContent;
+            if (empty($type) || $type == 'text') {
+                // Plain text source -- let's turn it into HTML!
+                return htmlspecialchars($contentEl->textContent);
             } else if ($type == 'html') {
-                $text = $contentEl->textContent;
-                return htmlspecialchars_decode($text, ENT_QUOTES);
+                // The XML text decoding gives us an HTML string ready to roll.
+                return $contentEl->textContent;
             } else if ($type == 'xhtml') {
-                $divEl = ActivityUtils::child($contentEl, 'div');
+                // Embedded XHTML; we have to pull it out of the document tree,
+                // then serialize it back out to an HTML fragment string.
+                $divEl = ActivityUtils::child($contentEl, 'div', 'http://www.w3.org/1999/xhtml');
                 if (empty($divEl)) {
                     return null;
                 }
@@ -403,7 +484,7 @@ class ActivityUtils
                     $text .= $doc->saveXML($child);
                 }
                 return trim($text);
-            } else if (in_array(array('text/xml', 'application/xml'), $type) ||
+            } else if (in_array($type, array('text/xml', 'application/xml')) ||
                        preg_match('#(+|/)xml$#', $type)) {
                 throw new ClientException(_("Can't handle embedded XML content yet."));
             } else if (strncasecmp($type, 'text/', 5)) {
@@ -415,6 +496,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
  *
@@ -471,7 +621,7 @@ class ActivityObject
     public $content;
     public $link;
     public $source;
-    public $avatar;
+    public $avatarLinks = array();
     public $geopoint;
     public $poco;
     public $displayName;
@@ -494,50 +644,181 @@ class ActivityObject
 
         $this->element = $element;
 
+        $this->geopoint = $this->_childContent(
+            $element,
+            ActivityContext::POINT,
+            ActivityContext::GEORSS
+        );
+
         if ($element->tagName == 'author') {
+            $this->_fromAuthor($element);
+        } else if ($element->tagName == 'item') {
+            $this->_fromRssItem($element);
+        } else {
+            $this->_fromAtomEntry($element);
+        }
 
-            $this->type  = self::PERSON; // XXX: is this fair?
-            $this->title = $this->_childContent($element, self::NAME);
-            $this->id    = $this->_childContent($element, self::URI);
+        // Some per-type attributes...
+        if ($this->type == self::PERSON || $this->type == self::GROUP) {
+            $this->displayName = $this->title;
 
-            if (empty($this->id)) {
-                $email = $this->_childContent($element, self::EMAIL);
-                if (!empty($email)) {
-                    // XXX: acct: ?
-                    $this->id = 'mailto:'.$email;
+            $photos = ActivityUtils::getLinks($element, 'photo');
+            if (count($photos)) {
+                foreach ($photos as $link) {
+                    $this->avatarLinks[] = new AvatarLink($link);
+                }
+            } else {
+                $avatars = ActivityUtils::getLinks($element, 'avatar');
+                foreach ($avatars as $link) {
+                    $this->avatarLinks[] = new AvatarLink($link);
                 }
             }
 
+            $this->poco = new PoCo($element);
+        }
+    }
+
+    private function _fromAuthor($element)
+    {
+        $this->type  = self::PERSON; // XXX: is this fair?
+        $this->title = $this->_childContent($element, self::NAME);
+        $this->id    = $this->_childContent($element, self::URI);
+
+        if (empty($this->id)) {
+            $email = $this->_childContent($element, self::EMAIL);
+            if (!empty($email)) {
+                // XXX: acct: ?
+                $this->id = 'mailto:'.$email;
+            }
+        }
+    }
+
+    private function _fromAtomEntry($element)
+    {
+        $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+                                           Activity::SPEC);
+
+        if (empty($this->type)) {
+            $this->type = ActivityObject::NOTE;
+        }
+
+        $this->id      = $this->_childContent($element, self::ID);
+        $this->title   = $this->_childContent($element, self::TITLE);
+        $this->summary = $this->_childContent($element, self::SUMMARY);
+
+        $this->source  = $this->_getSource($element);
+
+        $this->content = ActivityUtils::getContent($element);
+
+        $this->link = ActivityUtils::getPermalink($element);
+    }
+
+    // @fixme rationalize with Activity::_fromRssItem()
+
+    private function _fromRssItem($item)
+    {
+        $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
+
+        $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
+
+        if (!empty($contentEl)) {
+            $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
         } else {
+            $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
+            if (!empty($descriptionEl)) {
+                $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
+            }
+        }
+
+        $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
 
-            $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
-                                               Activity::SPEC);
+        $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
 
-            if (empty($this->type)) {
-                $this->type = ActivityObject::NOTE;
+        if (!empty($guidEl)) {
+            $this->id = $guidEl->textContent;
+
+            if ($guidEl->hasAttribute('isPermaLink')) {
+                // overwrites <link>
+                $this->link = $this->id;
             }
+        }
+    }
+
+    public static function fromRssAuthor($el)
+    {
+        $text = $el->textContent;
+
+        if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
+            $email = $match[1];
+            $name = $match[2];
+        } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
+            $name = $match[1];
+            $email = $match[2];
+        } else if (preg_match('/.*@.*/', $text)) {
+            $email = $text;
+            $name = null;
+        } else {
+            $name = $text;
+            $email = null;
+        }
 
-            $this->id      = $this->_childContent($element, self::ID);
-            $this->title   = $this->_childContent($element, self::TITLE);
-            $this->summary = $this->_childContent($element, self::SUMMARY);
+        // Not really enough info
 
-            $this->source  = $this->_getSource($element);
+        $obj = new ActivityObject();
 
-            $this->content = ActivityUtils::getContent($element);
+        $obj->element = $el;
 
-            $this->link = ActivityUtils::getPermalink($element);
+        $obj->type  = ActivityObject::PERSON;
+        $obj->title = $name;
 
+        if (!empty($email)) {
+            $obj->id = 'mailto:'.$email;
         }
 
-        // Some per-type attributes...
-        if ($this->type == self::PERSON || $this->type == self::GROUP) {
-            $this->displayName = $this->title;
+        return $obj;
+    }
 
-            // @fixme we may have multiple avatars with different resolutions specified
-            $this->avatar = ActivityUtils::getLink($element, 'avatar');
+    public static function fromDcCreator($el)
+    {
+        // Not really enough info
 
-            $this->poco = new PoCo($element);
+        $text = $el->textContent;
+
+        $obj = new ActivityObject();
+
+        $obj->element = $el;
+
+        $obj->title = $text;
+        $obj->type  = ActivityObject::PERSON;
+
+        return $obj;
+    }
+
+    public static function fromRssChannel($el)
+    {
+        $obj = new ActivityObject();
+
+        $obj->element = $el;
+
+        $obj->type = ActivityObject::PERSON; // @fixme guess better
+
+        $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, self::RSS);
+        $obj->link  = ActivityUtils::childContent($el, ActivityUtils::LINK, self::RSS);
+        $obj->id    = ActivityUtils::getLink($el, self::SELF);
+
+        $desc = ActivityUtils::childContent($el, self::DESCRIPTION, self::RSS);
+
+        if (!empty($desc)) {
+            $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
         }
+
+        $imageEl = ActivityUtils::child($el, self::IMAGE, self::RSS);
+
+        if (!empty($imageEl)) {
+            $obj->avatarLinks[] = ActivityUtils::childContent($imageEl, self::URL, self::RSS);
+        }
+
+        return $obj;
     }
 
     private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
@@ -563,7 +844,7 @@ class ActivityObject
         }
     }
 
-    static function fromNotice($notice)
+    static function fromNotice(Notice $notice)
     {
         $object = new ActivityObject();
 
@@ -577,7 +858,7 @@ class ActivityObject
         return $object;
     }
 
-    static function fromProfile($profile)
+    static function fromProfile(Profile $profile)
     {
         $object = new ActivityObject();
 
@@ -585,10 +866,40 @@ 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 = PoCo::fromProfile($profile);
@@ -596,6 +907,35 @@ class ActivityObject
         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);
@@ -607,16 +947,28 @@ class ActivityObject
         $xs->element(self::ID, null, $this->id);
 
         if (!empty($this->title)) {
-            $xs->element(self::TITLE, null, $this->title);
+            $xs->element(
+                self::TITLE,
+                null,
+                common_xml_safe_str($this->title)
+            );
         }
 
         if (!empty($this->summary)) {
-            $xs->element(self::SUMMARY, null, $this->summary);
+            $xs->element(
+                self::SUMMARY,
+                null,
+                common_xml_safe_str($this->summary)
+            );
         }
 
         if (!empty($this->content)) {
             // XXX: assuming HTML content here
-            $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content);
+            $xs->element(
+                ActivityUtils::CONTENT,
+                array('type' => 'html'),
+                common_xml_safe_str($this->content)
+            );
         }
 
         if (!empty($this->link)) {
@@ -633,16 +985,19 @@ class ActivityObject
 
         if ($this->type == ActivityObject::PERSON
             || $this->type == ActivityObject::GROUP) {
-            $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()
-                ),
-                null
-            );
+
+            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)) {
@@ -691,6 +1046,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
@@ -756,22 +1114,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;
+    }
 }
 
 /**
@@ -810,6 +1175,21 @@ class Activity
     const PUBLISHED = 'published';
     const UPDATED   = 'updated';
 
+    const RSS = null; // no namespace!
+
+    const PUBDATE     = 'pubDate';
+    const DESCRIPTION = 'description';
+    const GUID        = 'guid';
+    const SELF        = 'self';
+    const IMAGE       = 'image';
+    const URL         = 'url';
+
+    const DC = 'http://purl.org/dc/elements/1.1/';
+
+    const CREATOR = 'creator';
+
+    const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/';
+
     public $actor;   // an ActivityObject
     public $verb;    // a string (the URL)
     public $object;  // an ActivityObject
@@ -824,6 +1204,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
@@ -838,9 +1220,29 @@ class Activity
             return;
         }
 
+        // Insist on a feed's root DOMElement; don't allow a DOMDocument
+        if ($feed instanceof DOMDocument) {
+            throw new ClientException(
+                _("Expecting a root feed element but got a whole XML document.")
+            );
+        }
+
         $this->entry = $entry;
         $this->feed  = $feed;
 
+        if ($entry->namespaceURI == Activity::ATOM &&
+            $entry->localName == 'entry') {
+            $this->_fromAtomEntry($entry, $feed);
+        } else if ($entry->namespaceURI == Activity::RSS &&
+                   $entry->localName == 'item') {
+            $this->_fromRssItem($entry, $feed);
+        } else {
+            throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}");
+        }
+    }
+
+    function _fromAtomEntry($entry, $feed)
+    {
         $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
 
         if (!empty($pubEl)) {
@@ -912,6 +1314,81 @@ 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');
+        }
+    }
+
+    function _fromRssItem($item, $rss)
+    {
+        $verbEl = $this->_child($item, self::VERB);
+
+        if (!empty($verbEl)) {
+            $this->verb = trim($verbEl->textContent);
+        } else {
+            $this->verb = ActivityVerb::POST;
+            // XXX: do other implied stuff here
+        }
+
+        $pubDateEl = $this->_child($item, self::PUBDATE, self::RSS);
+
+        if (!empty($pubDateEl)) {
+            $this->time = strtotime($pubDateEl->textContent);
+        }
+
+        $authorEl = $this->_child($item, self::AUTHOR, self::RSS);
+
+        if (!empty($authorEl)) {
+            $this->actor = ActivityObject::fromRssAuthor($authorEl);
+        } else {
+            $dcCreatorEl = $this->_child($item, self::CREATOR, self::DC);
+            if (!empty($dcCreatorEl)) {
+                $this->actor = ActivityObject::fromDcCreator($dcCreatorEl);
+            } else if (!empty($rss)) {
+                $this->actor = ActivityObject::fromRssChannel($rss);
+            }
+        }
+
+        $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS);
+
+        $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, self::CONTENTNS);
+
+        if (!empty($contentEl)) {
+            $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
+        } else {
+            $descriptionEl = ActivityUtils::child($item, self::DESCRIPTION, self::RSS);
+            if (!empty($descriptionEl)) {
+                $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
+            }
+        }
+
+        $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, self::RSS);
+
+        // @fixme enclosures
+        // @fixme thumbnails... maybe
+
+        $guidEl = ActivityUtils::child($item, self::GUID, self::RSS);
+
+        if (!empty($guidEl)) {
+            $this->id = $guidEl->textContent;
+
+            if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') {
+                // overwrites <link>
+                $this->link = $this->id;
+            }
+        }
+
+        $this->object  = new ActivityObject($item);
+        $this->context = new ActivityContext($item);
     }
 
     /**
@@ -934,7 +1411,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();
         }
@@ -957,11 +1435,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');
 
@@ -972,4 +1467,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();
+    }
+}