]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/activityobject.php
Merge branch 'master' into mmn_fixes
[quix0rs-gnu-social.git] / lib / activityobject.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * An activity
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Feed
23  * @package   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @author    Zach Copley <zach@status.net>
26  * @copyright 2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
28  * @link      http://status.net/
29  */
30
31 if (!defined('GNUSOCIAL')) { exit(1); }
32
33 require_once(INSTALLDIR.'/lib/activitystreamjsondocument.php');
34
35 /**
36  * A noun-ish thing in the activity universe
37  *
38  * The activity streams spec talks about activity objects, while also having
39  * a tag activity:object, which is in fact an activity object. Aaaaaah!
40  *
41  * This is just a thing in the activity universe. Can be the subject, object,
42  * or indirect object (target!) of an activity verb. Rotten name, and I'm
43  * propagating it. *sigh*
44  *
45  * @category  OStatus
46  * @package   StatusNet
47  * @author    Evan Prodromou <evan@status.net>
48  * @copyright 2010 StatusNet, Inc.
49  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
50  * @link      http://status.net/
51  */
52 class ActivityObject
53 {
54     const ARTICLE   = 'http://activitystrea.ms/schema/1.0/article';
55     const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
56     const NOTE      = 'http://activitystrea.ms/schema/1.0/note';
57     const STATUS    = 'http://activitystrea.ms/schema/1.0/status';
58     const FILE      = 'http://activitystrea.ms/schema/1.0/file';
59     const PHOTO     = 'http://activitystrea.ms/schema/1.0/photo';
60     const ALBUM     = 'http://activitystrea.ms/schema/1.0/photo-album';
61     const PLAYLIST  = 'http://activitystrea.ms/schema/1.0/playlist';
62     const VIDEO     = 'http://activitystrea.ms/schema/1.0/video';
63     const AUDIO     = 'http://activitystrea.ms/schema/1.0/audio';
64     const BOOKMARK  = 'http://activitystrea.ms/schema/1.0/bookmark';
65     const PERSON    = 'http://activitystrea.ms/schema/1.0/person';
66     const GROUP     = 'http://activitystrea.ms/schema/1.0/group';
67     const _LIST     = 'http://activitystrea.ms/schema/1.0/list'; // LIST is reserved
68     const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
69     const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
70     // ^^^^^^^^^^ tea!
71     const ACTIVITY = 'http://activitystrea.ms/schema/1.0/activity';
72     const SERVICE   = 'http://activitystrea.ms/schema/1.0/service';
73     const IMAGE     = 'http://activitystrea.ms/schema/1.0/image';
74     const COLLECTION = 'http://activitystrea.ms/schema/1.0/collection';
75     const APPLICATION = 'http://activitystrea.ms/schema/1.0/application';
76
77     // Atom elements we snarf
78
79     const TITLE   = 'title';
80     const SUMMARY = 'summary';
81     const ID      = 'id';
82     const SOURCE  = 'source';
83
84     const NAME  = 'name';
85     const URI   = 'uri';
86     const EMAIL = 'email';
87
88     const POSTEROUS   = 'http://posterous.com/help/rss/1.0';
89     const AUTHOR      = 'author';
90     const USERIMAGE   = 'userImage';
91     const PROFILEURL  = 'profileUrl';
92     const NICKNAME    = 'nickName';
93     const DISPLAYNAME = 'displayName';
94
95     public $element;
96     public $type;
97     public $id;
98     public $title;
99     public $summary;
100     public $content;
101     public $owner;
102     public $link;
103     public $selfLink;   // think APP (Atom Publishing Protocol)
104     public $source;
105     public $avatarLinks = array();
106     public $geopoint;
107     public $poco;
108     public $displayName;
109
110     // @todo move this stuff to it's own PHOTO activity object
111     const MEDIA_DESCRIPTION = 'description';
112
113     public $thumbnail;
114     public $largerImage;
115     public $description;
116     public $extra = array();
117
118     public $stream;
119
120     /**
121      * Constructor
122      *
123      * This probably needs to be refactored
124      * to generate a local class (ActivityPerson, ActivityFile, ...)
125      * based on the object type.
126      *
127      * @param DOMElement $element DOM thing to turn into an Activity thing
128      */
129     function __construct($element = null)
130     {
131         if (empty($element)) {
132             return;
133         }
134
135         $this->element = $element;
136
137         $this->geopoint = $this->_childContent(
138             $element,
139             ActivityContext::POINT,
140             ActivityContext::GEORSS
141         );
142
143         if ($element->tagName == 'author') {
144             $this->_fromAuthor($element);
145         } else if ($element->tagName == 'item') {
146             $this->_fromRssItem($element);
147         } else {
148             $this->_fromAtomEntry($element);
149         }
150
151         // Some per-type attributes...
152         if ($this->type == self::PERSON || $this->type == self::GROUP) {
153             $this->displayName = $this->title;
154
155             $photos = ActivityUtils::getLinks($element, 'photo');
156             if (count($photos)) {
157                 foreach ($photos as $link) {
158                     $this->avatarLinks[] = new AvatarLink($link);
159                 }
160             } else {
161                 $avatars = ActivityUtils::getLinks($element, 'avatar');
162                 foreach ($avatars as $link) {
163                     $this->avatarLinks[] = new AvatarLink($link);
164                 }
165             }
166
167             $this->poco = new PoCo($element);
168         }
169
170         if ($this->type == self::PHOTO) {
171
172             $this->thumbnail   = ActivityUtils::getLink($element, 'preview');
173             $this->largerImage = ActivityUtils::getLink($element, 'enclosure');
174
175             $this->description = ActivityUtils::childContent(
176                 $element,
177                 ActivityObject::MEDIA_DESCRIPTION,
178                 Activity::MEDIA
179             );
180         }
181         if ($this->type == self::_LIST) {
182             $owner = ActivityUtils::child($this->element, Activity::AUTHOR, Activity::SPEC);
183             $this->owner = new ActivityObject($owner);
184         }
185     }
186
187     private function _fromAuthor($element)
188     {
189         $this->type = $this->_childContent($element,
190                                            Activity::OBJECTTYPE,
191                                            Activity::SPEC);
192
193         if (empty($this->type)) {
194             $this->type = self::PERSON; // XXX: is this fair?
195         }
196
197
198         // Start with <poco::displayName>
199
200         $this->title = ActivityUtils::childContent($element, PoCo::DISPLAYNAME, PoCo::NS);
201
202         // try falling back to <atom:title>
203
204         if (empty($this->title)) {
205             $title = ActivityUtils::childHtmlContent($element, self::TITLE);
206
207             if (!empty($title)) {
208                 $this->title = common_strip_html($title);
209             }
210         }
211
212         // fall back to <atom:name> as a last resort
213
214         if (empty($this->title)) {
215             $this->title = $this->_childContent($element, self::NAME);
216         }
217
218         // start with <atom:id>
219
220         $this->id = $this->_childContent($element, self::ID);
221
222         // fall back to <atom:uri>
223
224         if (empty($this->id)) {
225             $this->id = $this->_childContent($element, self::URI);
226         }
227
228         // fall further back to <atom:email>
229
230         if (empty($this->id)) {
231             $email = $this->_childContent($element, self::EMAIL);
232             if (!empty($email)) {
233                 // XXX: acct: ?
234                 $this->id = 'mailto:'.$email;
235             }
236         }
237
238         $this->link = ActivityUtils::getPermalink($element);
239
240         // fall finally back to <link rel=alternate>
241
242         if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
243             $this->id = $this->link;
244         }
245     }
246
247     private function _fromAtomEntry($element)
248     {
249         $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
250                                            Activity::SPEC);
251
252         if (empty($this->type)) {
253             $this->type = ActivityObject::NOTE;
254         }
255
256         $this->summary = ActivityUtils::childHtmlContent($element, self::SUMMARY);
257         $this->content = ActivityUtils::getContent($element);
258
259         // We don't like HTML in our titles, although it's technically allowed
260         $this->title = common_strip_html(ActivityUtils::childHtmlContent($element, self::TITLE));
261
262         $this->source  = $this->_getSource($element);
263
264         $this->link = ActivityUtils::getPermalink($element);
265         $this->selfLink = ActivityUtils::getSelfLink($element);
266
267         $this->id = $this->_childContent($element, self::ID);
268
269         if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
270             $this->id = $this->link;
271         }
272
273         $els = $element->childNodes;
274         $out = array();
275
276         for ($i = 0; $i < $els->length; $i++) {
277             $link = $els->item($i);
278             if ($link->localName == ActivityUtils::LINK && $link->namespaceURI == ActivityUtils::ATOM) {
279                 $attrs = array();
280                 foreach ($link->attributes as $attrName=>$attrNode) {
281                     $attrs[$attrName] = $attrNode->nodeValue;
282                 }
283                 $this->extra[] = [$link->localName,
284                                     $attrs,
285                                     $link->nodeValue];
286             }
287         }
288     }
289
290     // @todo FIXME: rationalize with Activity::_fromRssItem()
291     private function _fromRssItem($item)
292     {
293         if (empty($this->type)) {
294             $this->type = ActivityObject::NOTE;
295         }
296
297         $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, Activity::RSS);
298
299         $contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, Activity::CONTENTNS);
300
301         if (!empty($contentEl)) {
302             $this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES);
303         } else {
304             $descriptionEl = ActivityUtils::child($item, Activity::DESCRIPTION, Activity::RSS);
305             if (!empty($descriptionEl)) {
306                 $this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES);
307             }
308         }
309
310         $this->link = ActivityUtils::childContent($item, ActivityUtils::LINK, Activity::RSS);
311
312         $guidEl = ActivityUtils::child($item, Activity::GUID, Activity::RSS);
313
314         if (!empty($guidEl)) {
315             $this->id = $guidEl->textContent;
316
317             if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') {
318                 // overwrites <link>
319                 $this->link = $this->id;
320             }
321         }
322     }
323
324     public static function fromRssAuthor($el)
325     {
326         $text = $el->textContent;
327
328         if (preg_match('/^(.*?) \((.*)\)$/', $text, $match)) {
329             $email = $match[1];
330             $name = $match[2];
331         } else if (preg_match('/^(.*?) <(.*)>$/', $text, $match)) {
332             $name = $match[1];
333             $email = $match[2];
334         } else if (preg_match('/.*@.*/', $text)) {
335             $email = $text;
336             $name = null;
337         } else {
338             $name = $text;
339             $email = null;
340         }
341
342         // Not really enough info
343
344         $obj = new ActivityObject();
345
346         $obj->element = $el;
347
348         $obj->type  = ActivityObject::PERSON;
349         $obj->title = $name;
350
351         if (!empty($email)) {
352             $obj->id = 'mailto:'.$email;
353         }
354
355         return $obj;
356     }
357
358     public static function fromDcCreator($el)
359     {
360         // Not really enough info
361
362         $text = $el->textContent;
363
364         $obj = new ActivityObject();
365
366         $obj->element = $el;
367
368         $obj->title = $text;
369         $obj->type  = ActivityObject::PERSON;
370
371         return $obj;
372     }
373
374     public static function fromRssChannel($el)
375     {
376         $obj = new ActivityObject();
377
378         $obj->element = $el;
379
380         $obj->type = ActivityObject::PERSON; // @fixme guess better
381
382         $obj->title = ActivityUtils::childContent($el, ActivityObject::TITLE, Activity::RSS);
383         $obj->link  = ActivityUtils::childContent($el, ActivityUtils::LINK, Activity::RSS);
384         $obj->id    = ActivityUtils::getLink($el, Activity::SELF);
385
386         if (empty($obj->id)) {
387             $obj->id = $obj->link;
388         }
389
390         $desc = ActivityUtils::childContent($el, Activity::DESCRIPTION, Activity::RSS);
391
392         if (!empty($desc)) {
393             $obj->content = htmlspecialchars_decode($desc, ENT_QUOTES);
394         }
395
396         $imageEl = ActivityUtils::child($el, Activity::IMAGE, Activity::RSS);
397
398         if (!empty($imageEl)) {
399             $url = ActivityUtils::childContent($imageEl, Activity::URL, Activity::RSS);
400             $al = new AvatarLink();
401             $al->url = $url;
402             $obj->avatarLinks[] = $al;
403         }
404
405         return $obj;
406     }
407
408     public static function fromPosterousAuthor($el)
409     {
410         $obj = new ActivityObject();
411
412         $obj->type = ActivityObject::PERSON; // @fixme any others...?
413
414         $userImage = ActivityUtils::childContent($el, self::USERIMAGE, self::POSTEROUS);
415
416         if (!empty($userImage)) {
417             $al = new AvatarLink();
418             $al->url = $userImage;
419             $obj->avatarLinks[] = $al;
420         }
421
422         $obj->link = ActivityUtils::childContent($el, self::PROFILEURL, self::POSTEROUS);
423         $obj->id   = $obj->link;
424
425         $obj->poco = new PoCo();
426
427         $obj->poco->preferredUsername = ActivityUtils::childContent($el, self::NICKNAME, self::POSTEROUS);
428         $obj->poco->displayName       = ActivityUtils::childContent($el, self::DISPLAYNAME, self::POSTEROUS);
429
430         $obj->title = $obj->poco->displayName;
431
432         return $obj;
433     }
434
435     private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
436     {
437         return ActivityUtils::childContent($element, $tag, $namespace);
438     }
439
440     // Try to get a unique id for the source feed
441
442     private function _getSource($element)
443     {
444         $sourceEl = ActivityUtils::child($element, 'source');
445
446         if (empty($sourceEl)) {
447             return null;
448         } else {
449             $href = ActivityUtils::getLink($sourceEl, 'self');
450             if (!empty($href)) {
451                 return $href;
452             } else {
453                 return ActivityUtils::childContent($sourceEl, 'id');
454             }
455         }
456     }
457
458     static function fromGroup(User_group $group)
459     {
460         $object = new ActivityObject();
461
462         if (Event::handle('StartActivityObjectFromGroup', array($group, &$object))) {
463
464             $object->type   = ActivityObject::GROUP;
465             $object->id     = $group->getUri();
466             $object->title  = $group->getBestName();
467             $object->link   = $group->getUri();
468
469             $object->avatarLinks[] = AvatarLink::fromFilename($group->homepage_logo,
470                                                               AVATAR_PROFILE_SIZE);
471
472             $object->avatarLinks[] = AvatarLink::fromFilename($group->stream_logo,
473                                                               AVATAR_STREAM_SIZE);
474
475             $object->avatarLinks[] = AvatarLink::fromFilename($group->mini_logo,
476                                                               AVATAR_MINI_SIZE);
477
478             $object->poco = PoCo::fromGroup($group);
479             Event::handle('EndActivityObjectFromGroup', array($group, &$object));
480         }
481
482         return $object;
483     }
484
485     static function fromPeopletag($ptag)
486     {
487         $object = new ActivityObject();
488         if (Event::handle('StartActivityObjectFromPeopletag', array($ptag, &$object))) {
489             $object->type    = ActivityObject::_LIST;
490
491             $object->id      = $ptag->getUri();
492             $object->title   = $ptag->tag;
493             $object->summary = $ptag->description;
494             $object->link    = $ptag->homeUrl();
495             $object->owner   = Profile::getKV('id', $ptag->tagger);
496             $object->poco    = PoCo::fromProfile($object->owner);
497             Event::handle('EndActivityObjectFromPeopletag', array($ptag, &$object));
498         }
499         return $object;
500     }
501
502     static function fromFile(File $file)
503     {
504         $object = new ActivityObject();
505
506         if (Event::handle('StartActivityObjectFromFile', array($file, &$object))) {
507
508             $object->type = self::mimeTypeToObjectType($file->mimetype);
509             $object->id   = TagURI::mint(sprintf("file:%d", $file->id));
510             $object->link = $file->getAttachmentUrl();
511
512             if ($file->title) {
513                 $object->title = $file->title;
514             }
515
516             if ($file->date) {
517                 $object->date = $file->date;
518             }
519
520             try {
521                 $thumbnail = $file->getThumbnail();
522                 $object->thumbnail = $thumbnail;
523             } catch (UseFileAsThumbnailException $e) {
524                 $object->thumbnail = null;
525             } catch (UnsupportedMediaException $e) {
526                 $object->thumbnail = null;
527             }
528
529             switch (self::canonicalType($object->type)) {
530             case 'image':
531                 $object->largerImage = $file->getUrl();
532                 break;
533             case 'video':
534             case 'audio':
535                 $object->stream = $file->getUrl();
536                 break;
537             }
538
539             Event::handle('EndActivityObjectFromFile', array($file, &$object));
540         }
541
542         return $object;
543     }
544
545     static function fromNoticeSource(Notice_source $source)
546     {
547         $object = new ActivityObject();
548         $wellKnown = array('web', 'xmpp', 'mail', 'omb', 'system', 'api', 'ostatus',
549                            'activity', 'feed', 'mirror', 'twitter', 'facebook');
550
551         if (Event::handle('StartActivityObjectFromNoticeSource', array($source, &$object))) {
552             $object->type = ActivityObject::APPLICATION;
553
554             if (in_array($source->code, $wellKnown)) {
555                 // We use one ID for all well-known StatusNet sources
556                 $object->id = "tag:status.net,2009:notice-source:".$source->code;
557             } else if ($source->url) {
558                 // They registered with an URL
559                 $object->id = $source->url;
560             } else {
561                 // Locally-registered, no URL
562                 $object->id = TagURI::mint("notice-source:".$source->code);
563             }
564
565             if ($source->url) {
566                 $object->link = $source->url;
567             }
568
569             if ($source->name) {
570                 $object->title = $source->name;
571             } else {
572                 $object->title = $source->code;
573             }
574
575             if ($source->created) {
576                 $object->date = $source->created;
577             }
578             
579             $object->extra[] = array('status_net', array('source_code' => $source->code));
580
581             Event::handle('EndActivityObjectFromNoticeSource', array($source, &$object));
582         }
583
584         return $object;
585     }
586
587     static function fromMessage(Message $message)
588     {
589         $object = new ActivityObject();
590
591         if (Event::handle('StartActivityObjectFromMessage', array($message, &$object))) {
592
593             $object->type    = ActivityObject::NOTE;
594             $object->id      = ($message->uri) ? $message->uri : (($message->url) ? $message->url : TagURI::mint(sprintf("message:%d", $message->id)));
595             $object->content = $message->rendered;
596             $object->date    = $message->created;
597
598             if ($message->url) {
599                 $object->link = $message->url;
600             } else {
601                 $object->link = common_local_url('showmessage', array('message' => $message->id));
602             }
603
604             $object->extra[] = array('status_net', array('message_id' => $message->id));
605             
606             Event::handle('EndActivityObjectFromMessage', array($message, &$object));
607         }
608
609         return $object;
610     }
611
612     function outputTo($xo, $tag='activity:object')
613     {
614         if (!empty($tag)) {
615             $xo->elementStart($tag);
616         }
617
618         if (Event::handle('StartActivityObjectOutputAtom', array($this, $xo))) {
619             $xo->element('activity:object-type', null, $this->type);
620
621             // <author> uses URI
622
623             if ($tag == 'author') {
624                 $xo->element(self::URI, null, $this->id);
625             } else {
626                 $xo->element(self::ID, null, $this->id);
627             }
628
629             if (!empty($this->title)) {
630                 $name = common_xml_safe_str($this->title);
631                 if ($tag == 'author') {
632                     // XXX: Backward compatibility hack -- atom:name should contain
633                     // full name here, instead of nickname, i.e.: $name. Change
634                     // this in the next version.
635                     $xo->element(self::NAME, null, $this->poco->preferredUsername);
636                 } else {
637                     $xo->element(self::TITLE, null, $name);
638                 }
639             }
640
641             if (!empty($this->summary)) {
642                 $xo->element(
643                     self::SUMMARY,
644                     null,
645                     common_xml_safe_str($this->summary)
646                 );
647             }
648
649             if (!empty($this->content)) {
650                 // XXX: assuming HTML content here
651                 $xo->element(
652                     ActivityUtils::CONTENT,
653                     array('type' => 'html'),
654                     common_xml_safe_str($this->content)
655                 );
656             }
657
658             if (!empty($this->link)) {
659                 $xo->element(
660                     'link',
661                     array(
662                         'rel' => 'alternate',
663                         'type' => 'text/html',
664                         'href' => $this->link
665                     ),
666                     null
667                 );
668             }
669
670             if (!empty($this->selfLink)) {
671                 $xo->element(
672                     'link',
673                     array(
674                         'rel' => 'self',
675                         'type' => 'application/atom+xml',
676                         'href' => $this->selfLink
677                     ),
678                     null
679                 );
680             }
681
682             if(!empty($this->owner)) {
683                 $owner = $this->owner->asActivityNoun(self::AUTHOR);
684                 $xo->raw($owner);
685             }
686
687             if ($this->type == ActivityObject::PERSON
688                 || $this->type == ActivityObject::GROUP) {
689
690                 foreach ($this->avatarLinks as $alink) {
691                     $xo->element('link',
692                             array(
693                                 'rel'          => 'avatar',
694                                 'type'         => $alink->type,
695                                 'media:width'  => $alink->width,
696                                 'media:height' => $alink->height,
697                                 'href'         => $alink->url,
698                                 ),
699                             null);
700                 }
701             }
702
703             if (!empty($this->geopoint)) {
704                 $xo->element(
705                     'georss:point',
706                     null,
707                     $this->geopoint
708                 );
709             }
710
711             if (!empty($this->poco)) {
712                 $this->poco->outputTo($xo);
713             }
714
715             // @fixme there's no way here to make a tree; elements can only contain plaintext
716             // @fixme these may collide with JSON extensions
717             foreach ($this->extra as $el) {
718                 list($extraTag, $attrs, $content) = array_pad($el, 3, null);
719                 $xo->element($extraTag, $attrs, $content);
720             }
721
722             Event::handle('EndActivityObjectOutputAtom', array($this, $xo));
723         }
724
725         if (!empty($tag)) {
726             $xo->elementEnd($tag);
727         }
728
729         return;
730     }
731
732     function asString($tag='activity:object')
733     {
734         $xs = new XMLStringer(true);
735
736         $this->outputTo($xs, $tag);
737
738         return $xs->getString();
739     }
740
741     /*
742      * Returns an array based on this Activity Object suitable for
743      * encoding as JSON.
744      *
745      * @return array $object the activity object array
746      */
747
748     function asArray()
749     {
750         $object = array();
751
752         if (Event::handle('StartActivityObjectOutputJson', array($this, &$object))) {
753             // XXX: attachments are added by Activity
754
755             // author (Add object for author? Could be useful for repeats.)
756
757             // content (Add rendered version of the notice?)
758
759             // downstreamDuplicates
760
761             // id
762
763             if ($this->id) {
764                 $object['id'] = $this->id;
765             } else if ($this->link) {
766                 $object['id'] = $this->link;
767             }
768
769             if ($this->type == ActivityObject::PERSON
770                 || $this->type == ActivityObject::GROUP) {
771
772                 // displayName
773                 $object['displayName'] = $this->title;
774
775                 // XXX: Not sure what the best avatar is to use for the
776                 // author's "image". For now, I'm using the large size.
777
778                 $imgLink          = null;
779                 $avatarMediaLinks = array();
780
781                 foreach ($this->avatarLinks as $a) {
782
783                     // Make a MediaLink for every other Avatar
784                     $avatar = new ActivityStreamsMediaLink(
785                         $a->url,
786                         $a->width,
787                         $a->height,
788                         $a->type,
789                         'avatar'
790                     );
791
792                     // Find the big avatar to use as the "image"
793                     if ($a->height == AVATAR_PROFILE_SIZE) {
794                         $imgLink = $avatar;
795                     }
796
797                     $avatarMediaLinks[] = $avatar->asArray();
798                 }
799
800                 if (!array_key_exists('status_net', $object)) {
801                     $object['status_net'] = array();
802                 }
803
804                 $object['status_net']['avatarLinks'] = $avatarMediaLinks; // extension
805
806                 // image
807                 if (!empty($imgLink)) {
808                     $object['image']  = $imgLink->asArray();
809                 }
810             }
811
812             // objectType
813             //
814             // We can probably use the whole schema URL here but probably the
815             // relative simple name is easier to parse
816
817             $object['objectType'] = self::canonicalType($this->type);
818
819             // summary
820             $object['summary'] = $this->summary;
821
822             // content, usually rendered HTML
823             $object['content'] = $this->content;
824
825             // published (probably don't need. Might be useful for repeats.)
826
827             // updated (probably don't need this)
828
829             // TODO: upstreamDuplicates
830
831             if ($this->link) {
832                 $object['url'] = $this->link;
833             }
834
835             /* Extensions */
836             // @fixme these may collide with XML extensions
837             // @fixme multiple tags of same name will overwrite each other
838             // @fixme text content from XML extensions will be lost
839
840             foreach ($this->extra as $e) {
841                 list($objectName, $props, $txt) = array_pad($e, 3, null);
842                 if (!empty($objectName)) {
843                     $parts = explode(":", $objectName);
844                     if (count($parts) == 2 && $parts[0] == "statusnet") {
845                         if (!array_key_exists('status_net', $object)) {
846                             $object['status_net'] = array();
847                         }
848                         $object['status_net'][$parts[1]] = $props;
849                     } else {
850                         $object[$objectName] = $props;
851                     }
852                 }
853             }
854
855             if (!empty($this->geopoint)) {
856
857                 list($lat, $lon) = explode(' ', $this->geopoint);
858
859                 if (!empty($lat) && !empty($lon)) {
860                     $object['location'] = array(
861                         'objectType' => 'place',
862                         'position' => sprintf("%+02.5F%+03.5F/", $lat, $lon),
863                         'lat' => $lat,
864                         'lon' => $lon
865                     );
866
867                     $loc = Location::fromLatLon((float)$lat, (float)$lon);
868
869                     if ($loc) {
870                         $name = $loc->getName();
871
872                         if ($name) {
873                             $object['location']['displayName'] = $name;
874                         }
875                         $url = $loc->getURL();
876
877                         if ($url) {
878                             $object['location']['url'] = $url;
879                         }
880                     }
881                 }
882             }
883
884             if (!empty($this->poco)) {
885                 $object['portablecontacts_net'] = array_filter($this->poco->asArray());
886             }
887
888             if (!empty($this->thumbnail)) {
889                 if (is_string($this->thumbnail)) {
890                     $object['image'] = array('url' => $this->thumbnail);
891                 } else {
892                     $object['image'] = array('url' => $this->thumbnail->getUrl());
893                     if ($this->thumbnail->width) {
894                         $object['image']['width'] = $this->thumbnail->width;
895                     }
896                     if ($this->thumbnail->height) {
897                         $object['image']['height'] = $this->thumbnail->height;
898                     }
899                 }
900             }
901
902             switch (self::canonicalType($this->type)) {
903             case 'image':
904                 if (!empty($this->largerImage)) {
905                     $object['fullImage'] = array('url' => $this->largerImage);
906                 }
907                 break;
908             case 'audio':
909             case 'video':
910                 if (!empty($this->stream)) {
911                     $object['stream'] = array('url' => $this->stream);
912                 }
913                 break;
914             }
915
916             Event::handle('EndActivityObjectOutputJson', array($this, &$object));
917         }
918         return array_filter($object);
919     }
920
921     public function getIdentifiers() {
922         $ids = array();
923         foreach(array('id', 'link', 'url') as $id) {
924             if (isset($this->$id)) {
925                 $ids[] = $this->$id;
926             }
927         }
928         return array_unique($ids);
929     }
930
931     static function canonicalType($type) {
932         return ActivityUtils::resolveUri($type, true);
933     }
934
935     static function mimeTypeToObjectType($mimeType) {
936         $ot = null;
937
938         // Default
939
940         if (empty($mimeType)) {
941             return self::FILE;
942         }
943
944         $parts = explode('/', $mimeType);
945
946         switch ($parts[0]) {
947         case 'image':
948             $ot = self::IMAGE;
949             break;
950         case 'audio':
951             $ot = self::AUDIO;
952             break;
953         case 'video':
954             $ot = self::VIDEO;
955             break;
956         default:
957             $ot = self::FILE;
958         }
959
960         return $ot;
961     }
962 }