]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Finish api/statusnet/groups/update
[quix0rs-gnu-social.git] / lib / apiaction.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Base API action
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  API
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Dan Moore <dan@moore.cx>
26  * @author    Evan Prodromou <evan@status.net>
27  * @author    Jeffery To <jeffery.to@gmail.com>
28  * @author    Toby Inkster <mail@tobyinkster.co.uk>
29  * @author    Zach Copley <zach@status.net>
30  * @copyright 2009 StatusNet, Inc.
31  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
32  * @link      http://status.net/
33  */
34
35 /* External API usage documentation. Please update when you change how the API works. */
36
37 /*! @mainpage StatusNet REST API
38
39     @section Introduction
40
41     Some explanatory text about the API would be nice.
42
43     @section API Methods
44
45     @subsection timelinesmethods_sec Timeline Methods
46
47     @li @ref publictimeline
48     @li @ref friendstimeline
49
50     @subsection statusmethods_sec Status Methods
51
52     @li @ref statusesupdate
53
54     @subsection usermethods_sec User Methods
55
56     @subsection directmessagemethods_sec Direct Message Methods
57
58     @subsection friendshipmethods_sec Friendship Methods
59
60     @subsection socialgraphmethods_sec Social Graph Methods
61
62     @subsection accountmethods_sec Account Methods
63
64     @subsection favoritesmethods_sec Favorites Methods
65
66     @subsection blockmethods_sec Block Methods
67
68     @subsection oauthmethods_sec OAuth Methods
69
70     @subsection helpmethods_sec Help Methods
71
72     @subsection groupmethods_sec Group Methods
73
74     @page apiroot API Root
75
76     The URLs for methods referred to in this API documentation are
77     relative to the StatusNet API root. The API root is determined by the
78     site's @b server and @b path variables, which are generally specified
79     in config.php. For example:
80
81     @code
82     $config['site']['server'] = 'example.org';
83     $config['site']['path'] = 'statusnet'
84     @endcode
85
86     The pattern for a site's API root is: @c protocol://server/path/api E.g:
87
88     @c http://example.org/statusnet/api
89
90     The @b path can be empty.  In that case the API root would simply be:
91
92     @c http://example.org/api
93
94 */
95
96 if (!defined('STATUSNET')) {
97     exit(1);
98 }
99
100 class ApiValidationException extends Exception { }
101
102 /**
103  * Contains most of the Twitter-compatible API output functions.
104  *
105  * @category API
106  * @package  StatusNet
107  * @author   Craig Andrews <candrews@integralblue.com>
108  * @author   Dan Moore <dan@moore.cx>
109  * @author   Evan Prodromou <evan@status.net>
110  * @author   Jeffery To <jeffery.to@gmail.com>
111  * @author   Toby Inkster <mail@tobyinkster.co.uk>
112  * @author   Zach Copley <zach@status.net>
113  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
114  * @link     http://status.net/
115  */
116
117 class ApiAction extends Action
118 {
119     const READ_ONLY  = 1;
120     const READ_WRITE = 2;
121
122     var $format    = null;
123     var $user      = null;
124     var $auth_user = null;
125     var $page      = null;
126     var $count     = null;
127     var $max_id    = null;
128     var $since_id  = null;
129
130     var $access    = self::READ_ONLY;  // read (default) or read-write
131
132     /**
133      * Initialization.
134      *
135      * @param array $args Web and URL arguments
136      *
137      * @return boolean false if user doesn't exist
138      */
139
140     function prepare($args)
141     {
142         StatusNet::setApi(true); // reduce exception reports to aid in debugging
143         parent::prepare($args);
144
145         $this->format   = $this->arg('format');
146         $this->page     = (int)$this->arg('page', 1);
147         $this->count    = (int)$this->arg('count', 20);
148         $this->max_id   = (int)$this->arg('max_id', 0);
149         $this->since_id = (int)$this->arg('since_id', 0);
150
151         if ($this->arg('since')) {
152             header('X-StatusNet-Warning: since parameter is disabled; use since_id');
153         }
154
155         return true;
156     }
157
158     /**
159      * Handle a request
160      *
161      * @param array $args Arguments from $_REQUEST
162      *
163      * @return void
164      */
165
166     function handle($args)
167     {
168         header('Access-Control-Allow-Origin: *');
169         parent::handle($args);
170     }
171
172     /**
173      * Overrides XMLOutputter::element to write booleans as strings (true|false).
174      * See that method's documentation for more info.
175      *
176      * @param string $tag     Element type or tagname
177      * @param array  $attrs   Array of element attributes, as
178      *                        key-value pairs
179      * @param string $content string content of the element
180      *
181      * @return void
182      */
183     function element($tag, $attrs=null, $content=null)
184     {
185         if (is_bool($content)) {
186             $content = ($content ? 'true' : 'false');
187         }
188
189         return parent::element($tag, $attrs, $content);
190     }
191
192     function twitterUserArray($profile, $get_notice=false)
193     {
194         $twitter_user = array();
195
196         $twitter_user['id'] = intval($profile->id);
197         $twitter_user['name'] = $profile->getBestName();
198         $twitter_user['screen_name'] = $profile->nickname;
199         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
200         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
201
202         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
203         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
204             Avatar::defaultImage(AVATAR_STREAM_SIZE);
205
206         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
207         $twitter_user['protected'] = false; # not supported by StatusNet yet
208         $twitter_user['followers_count'] = $profile->subscriberCount();
209
210         $design        = null;
211         $user          = $profile->getUser();
212
213         // Note: some profiles don't have an associated user
214
215         $defaultDesign = Design::siteDesign();
216
217         if (!empty($user)) {
218             $design = $user->getDesign();
219         }
220
221         if (empty($design)) {
222             $design = $defaultDesign;
223         }
224
225         $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
226         $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
227         $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
228         $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
229         $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
230         $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
231         $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
232         $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
233         $twitter_user['profile_sidebar_border_color'] = '';
234
235         $twitter_user['friends_count'] = $profile->subscriptionCount();
236
237         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
238
239         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
240
241         $timezone = 'UTC';
242
243         if (!empty($user) && $user->timezone) {
244             $timezone = $user->timezone;
245         }
246
247         $t = new DateTime;
248         $t->setTimezone(new DateTimeZone($timezone));
249
250         $twitter_user['utc_offset'] = $t->format('Z');
251         $twitter_user['time_zone'] = $timezone;
252
253         $twitter_user['profile_background_image_url']
254             = empty($design->backgroundimage)
255             ? '' : ($design->disposition & BACKGROUND_ON)
256             ? Design::url($design->backgroundimage) : '';
257
258         $twitter_user['profile_background_tile']
259             = empty($design->disposition)
260             ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
261
262         $twitter_user['statuses_count'] = $profile->noticeCount();
263
264         // Is the requesting user following this user?
265         $twitter_user['following'] = false;
266         $twitter_user['notifications'] = false;
267
268         if (isset($this->auth_user)) {
269
270             $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
271
272             // Notifications on?
273             $sub = Subscription::pkeyGet(array('subscriber' =>
274                                                $this->auth_user->id,
275                                                'subscribed' => $profile->id));
276
277             if ($sub) {
278                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
279             }
280         }
281
282         if ($get_notice) {
283             $notice = $profile->getCurrentNotice();
284             if ($notice) {
285                 # don't get user!
286                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
287             }
288         }
289
290         return $twitter_user;
291     }
292
293     function twitterStatusArray($notice, $include_user=true)
294     {
295         $base = $this->twitterSimpleStatusArray($notice, $include_user);
296
297         if (!empty($notice->repeat_of)) {
298             $original = Notice::staticGet('id', $notice->repeat_of);
299             if (!empty($original)) {
300                 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
301                 $base['retweeted_status'] = $original_array;
302             }
303         }
304
305         return $base;
306     }
307
308     function twitterSimpleStatusArray($notice, $include_user=true)
309     {
310         $profile = $notice->getProfile();
311
312         $twitter_status = array();
313         $twitter_status['text'] = $notice->content;
314         $twitter_status['truncated'] = false; # Not possible on StatusNet
315         $twitter_status['created_at'] = $this->dateTwitter($notice->created);
316         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
317             intval($notice->reply_to) : null;
318
319         $source = null;
320
321         $ns = $notice->getSource();
322         if ($ns) {
323             if (!empty($ns->name) && !empty($ns->url)) {
324                 $source = '<a href="'
325                     . htmlspecialchars($ns->url)
326                     . '" rel="nofollow">'
327                     . htmlspecialchars($ns->name)
328                     . '</a>';
329             } else {
330                 $source = $ns->code;
331             }
332         }
333
334         $twitter_status['source'] = $source;
335         $twitter_status['id'] = intval($notice->id);
336
337         $replier_profile = null;
338
339         if ($notice->reply_to) {
340             $reply = Notice::staticGet(intval($notice->reply_to));
341             if ($reply) {
342                 $replier_profile = $reply->getProfile();
343             }
344         }
345
346         $twitter_status['in_reply_to_user_id'] =
347             ($replier_profile) ? intval($replier_profile->id) : null;
348         $twitter_status['in_reply_to_screen_name'] =
349             ($replier_profile) ? $replier_profile->nickname : null;
350
351         if (isset($notice->lat) && isset($notice->lon)) {
352             // This is the format that GeoJSON expects stuff to be in
353             $twitter_status['geo'] = array('type' => 'Point',
354                                            'coordinates' => array((float) $notice->lat,
355                                                                   (float) $notice->lon));
356         } else {
357             $twitter_status['geo'] = null;
358         }
359
360         if (isset($this->auth_user)) {
361             $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
362         } else {
363             $twitter_status['favorited'] = false;
364         }
365
366         // Enclosures
367         $attachments = $notice->attachments();
368
369         if (!empty($attachments)) {
370
371             $twitter_status['attachments'] = array();
372
373             foreach ($attachments as $attachment) {
374                 $enclosure_o=$attachment->getEnclosure();
375                 if ($enclosure_o) {
376                     $enclosure = array();
377                     $enclosure['url'] = $enclosure_o->url;
378                     $enclosure['mimetype'] = $enclosure_o->mimetype;
379                     $enclosure['size'] = $enclosure_o->size;
380                     $twitter_status['attachments'][] = $enclosure;
381                 }
382             }
383         }
384
385         if ($include_user && $profile) {
386             # Don't get notice (recursive!)
387             $twitter_user = $this->twitterUserArray($profile, false);
388             $twitter_status['user'] = $twitter_user;
389         }
390
391         return $twitter_status;
392     }
393
394     function twitterGroupArray($group)
395     {
396         $twitter_group=array();
397         $twitter_group['id']=$group->id;
398         $twitter_group['url']=$group->permalink();
399         $twitter_group['nickname']=$group->nickname;
400         $twitter_group['fullname']=$group->fullname;
401         $twitter_group['original_logo']=$group->original_logo;
402         $twitter_group['homepage_logo']=$group->homepage_logo;
403         $twitter_group['stream_logo']=$group->stream_logo;
404         $twitter_group['mini_logo']=$group->mini_logo;
405         $twitter_group['homepage']=$group->homepage;
406         $twitter_group['description']=$group->description;
407         $twitter_group['location']=$group->location;
408         $twitter_group['created']=$this->dateTwitter($group->created);
409         $twitter_group['modified']=$this->dateTwitter($group->modified);
410         return $twitter_group;
411     }
412
413     function twitterRssGroupArray($group)
414     {
415         $entry = array();
416         $entry['content']=$group->description;
417         $entry['title']=$group->nickname;
418         $entry['link']=$group->permalink();
419         $entry['published']=common_date_iso8601($group->created);
420         $entry['updated']==common_date_iso8601($group->modified);
421         $taguribase = common_config('integration', 'groupuri');
422         $entry['id'] = "group:$groupuribase:$entry[link]";
423
424         $entry['description'] = $entry['content'];
425         $entry['pubDate'] = common_date_rfc2822($group->created);
426         $entry['guid'] = $entry['link'];
427
428         return $entry;
429     }
430
431     function twitterRssEntryArray($notice)
432     {
433         $profile = $notice->getProfile();
434         $entry = array();
435
436         // We trim() to avoid extraneous whitespace in the output
437
438         $entry['content'] = common_xml_safe_str(trim($notice->rendered));
439         $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
440         $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
441         $entry['published'] = common_date_iso8601($notice->created);
442
443         $taguribase = TagURI::base();
444         $entry['id'] = "tag:$taguribase:$entry[link]";
445
446         $entry['updated'] = $entry['published'];
447         $entry['author'] = $profile->getBestName();
448
449         // Enclosures
450         $attachments = $notice->attachments();
451         $enclosures = array();
452
453         foreach ($attachments as $attachment) {
454             $enclosure_o=$attachment->getEnclosure();
455             if ($enclosure_o) {
456                  $enclosure = array();
457                  $enclosure['url'] = $enclosure_o->url;
458                  $enclosure['mimetype'] = $enclosure_o->mimetype;
459                  $enclosure['size'] = $enclosure_o->size;
460                  $enclosures[] = $enclosure;
461             }
462         }
463
464         if (!empty($enclosures)) {
465             $entry['enclosures'] = $enclosures;
466         }
467
468         // Tags/Categories
469         $tag = new Notice_tag();
470         $tag->notice_id = $notice->id;
471         if ($tag->find()) {
472             $entry['tags']=array();
473             while ($tag->fetch()) {
474                 $entry['tags'][]=$tag->tag;
475             }
476         }
477         $tag->free();
478
479         // RSS Item specific
480         $entry['description'] = $entry['content'];
481         $entry['pubDate'] = common_date_rfc2822($notice->created);
482         $entry['guid'] = $entry['link'];
483
484         if (isset($notice->lat) && isset($notice->lon)) {
485             // This is the format that GeoJSON expects stuff to be in.
486             // showGeoRSS() below uses it for XML output, so we reuse it
487             $entry['geo'] = array('type' => 'Point',
488                                   'coordinates' => array((float) $notice->lat,
489                                                          (float) $notice->lon));
490         } else {
491             $entry['geo'] = null;
492         }
493
494         return $entry;
495     }
496
497     function twitterRelationshipArray($source, $target)
498     {
499         $relationship = array();
500
501         $relationship['source'] =
502             $this->relationshipDetailsArray($source, $target);
503         $relationship['target'] =
504             $this->relationshipDetailsArray($target, $source);
505
506         return array('relationship' => $relationship);
507     }
508
509     function relationshipDetailsArray($source, $target)
510     {
511         $details = array();
512
513         $details['screen_name'] = $source->nickname;
514         $details['followed_by'] = $target->isSubscribed($source);
515         $details['following'] = $source->isSubscribed($target);
516
517         $notifications = false;
518
519         if ($source->isSubscribed($target)) {
520
521             $sub = Subscription::pkeyGet(array('subscriber' =>
522                 $source->id, 'subscribed' => $target->id));
523
524             if (!empty($sub)) {
525                 $notifications = ($sub->jabber || $sub->sms);
526             }
527         }
528
529         $details['notifications_enabled'] = $notifications;
530         $details['blocking'] = $source->hasBlocked($target);
531         $details['id'] = $source->id;
532
533         return $details;
534     }
535
536     function showTwitterXmlRelationship($relationship)
537     {
538         $this->elementStart('relationship');
539
540         foreach($relationship as $element => $value) {
541             if ($element == 'source' || $element == 'target') {
542                 $this->elementStart($element);
543                 $this->showXmlRelationshipDetails($value);
544                 $this->elementEnd($element);
545             }
546         }
547
548         $this->elementEnd('relationship');
549     }
550
551     function showXmlRelationshipDetails($details)
552     {
553         foreach($details as $element => $value) {
554             $this->element($element, null, $value);
555         }
556     }
557
558     function showTwitterXmlStatus($twitter_status, $tag='status')
559     {
560         $this->elementStart($tag);
561         foreach($twitter_status as $element => $value) {
562             switch ($element) {
563             case 'user':
564                 $this->showTwitterXmlUser($twitter_status['user']);
565                 break;
566             case 'text':
567                 $this->element($element, null, common_xml_safe_str($value));
568                 break;
569             case 'attachments':
570                 $this->showXmlAttachments($twitter_status['attachments']);
571                 break;
572             case 'geo':
573                 $this->showGeoXML($value);
574                 break;
575             case 'retweeted_status':
576                 $this->showTwitterXmlStatus($value, 'retweeted_status');
577                 break;
578             default:
579                 $this->element($element, null, $value);
580             }
581         }
582         $this->elementEnd($tag);
583     }
584
585     function showTwitterXmlGroup($twitter_group)
586     {
587         $this->elementStart('group');
588         foreach($twitter_group as $element => $value) {
589             $this->element($element, null, $value);
590         }
591         $this->elementEnd('group');
592     }
593
594     function showTwitterXmlUser($twitter_user, $role='user')
595     {
596         $this->elementStart($role);
597         foreach($twitter_user as $element => $value) {
598             if ($element == 'status') {
599                 $this->showTwitterXmlStatus($twitter_user['status']);
600             } else {
601                 $this->element($element, null, $value);
602             }
603         }
604         $this->elementEnd($role);
605     }
606
607     function showXmlAttachments($attachments) {
608         if (!empty($attachments)) {
609             $this->elementStart('attachments', array('type' => 'array'));
610             foreach ($attachments as $attachment) {
611                 $attrs = array();
612                 $attrs['url'] = $attachment['url'];
613                 $attrs['mimetype'] = $attachment['mimetype'];
614                 $attrs['size'] = $attachment['size'];
615                 $this->element('enclosure', $attrs, '');
616             }
617             $this->elementEnd('attachments');
618         }
619     }
620
621     function showGeoXML($geo)
622     {
623         if (empty($geo)) {
624             // empty geo element
625             $this->element('geo');
626         } else {
627             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
628             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
629             $this->elementEnd('geo');
630         }
631     }
632
633     function showGeoRSS($geo)
634     {
635         if (!empty($geo)) {
636             $this->element(
637                 'georss:point',
638                 null,
639                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
640             );
641         }
642     }
643
644     function showTwitterRssItem($entry)
645     {
646         $this->elementStart('item');
647         $this->element('title', null, $entry['title']);
648         $this->element('description', null, $entry['description']);
649         $this->element('pubDate', null, $entry['pubDate']);
650         $this->element('guid', null, $entry['guid']);
651         $this->element('link', null, $entry['link']);
652
653         # RSS only supports 1 enclosure per item
654         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
655             $enclosure = $entry['enclosures'][0];
656             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
657         }
658
659         if(array_key_exists('tags', $entry)){
660             foreach($entry['tags'] as $tag){
661                 $this->element('category', null,$tag);
662             }
663         }
664
665         $this->showGeoRSS($entry['geo']);
666         $this->elementEnd('item');
667     }
668
669     function showJsonObjects($objects)
670     {
671         print(json_encode($objects));
672     }
673
674     function showSingleXmlStatus($notice)
675     {
676         $this->initDocument('xml');
677         $twitter_status = $this->twitterStatusArray($notice);
678         $this->showTwitterXmlStatus($twitter_status);
679         $this->endDocument('xml');
680     }
681
682     function show_single_json_status($notice)
683     {
684         $this->initDocument('json');
685         $status = $this->twitterStatusArray($notice);
686         $this->showJsonObjects($status);
687         $this->endDocument('json');
688     }
689
690     function showXmlTimeline($notice)
691     {
692
693         $this->initDocument('xml');
694         $this->elementStart('statuses', array('type' => 'array'));
695
696         if (is_array($notice)) {
697             foreach ($notice as $n) {
698                 $twitter_status = $this->twitterStatusArray($n);
699                 $this->showTwitterXmlStatus($twitter_status);
700             }
701         } else {
702             while ($notice->fetch()) {
703                 $twitter_status = $this->twitterStatusArray($notice);
704                 $this->showTwitterXmlStatus($twitter_status);
705             }
706         }
707
708         $this->elementEnd('statuses');
709         $this->endDocument('xml');
710     }
711
712     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
713     {
714
715         $this->initDocument('rss');
716
717         $this->element('title', null, $title);
718         $this->element('link', null, $link);
719
720         if (!is_null($self)) {
721             $this->element(
722                 'atom:link',
723                 array(
724                     'type' => 'application/rss+xml',
725                     'href' => $self,
726                     'rel'  => 'self'
727                 )
728            );
729         }
730
731         if (!is_null($suplink)) {
732             // For FriendFeed's SUP protocol
733             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
734                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
735                                          'href' => $suplink,
736                                          'type' => 'application/json'));
737         }
738
739         if (!is_null($logo)) {
740             $this->elementStart('image');
741             $this->element('link', null, $link);
742             $this->element('title', null, $title);
743             $this->element('url', null, $logo);
744             $this->elementEnd('image');
745         }
746
747         $this->element('description', null, $subtitle);
748         $this->element('language', null, 'en-us');
749         $this->element('ttl', null, '40');
750
751         if (is_array($notice)) {
752             foreach ($notice as $n) {
753                 $entry = $this->twitterRssEntryArray($n);
754                 $this->showTwitterRssItem($entry);
755             }
756         } else {
757             while ($notice->fetch()) {
758                 $entry = $this->twitterRssEntryArray($notice);
759                 $this->showTwitterRssItem($entry);
760             }
761         }
762
763         $this->endTwitterRss();
764     }
765
766     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
767     {
768
769         $this->initDocument('atom');
770
771         $this->element('title', null, $title);
772         $this->element('id', null, $id);
773         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
774
775         if (!is_null($logo)) {
776             $this->element('logo',null,$logo);
777         }
778
779         if (!is_null($suplink)) {
780             # For FriendFeed's SUP protocol
781             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
782                                          'href' => $suplink,
783                                          'type' => 'application/json'));
784         }
785
786         if (!is_null($selfuri)) {
787             $this->element('link', array('href' => $selfuri,
788                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
789         }
790
791         $this->element('updated', null, common_date_iso8601('now'));
792         $this->element('subtitle', null, $subtitle);
793
794         if (is_array($notice)) {
795             foreach ($notice as $n) {
796                 $this->raw($n->asAtomEntry());
797             }
798         } else {
799             while ($notice->fetch()) {
800                 $this->raw($notice->asAtomEntry());
801             }
802         }
803
804         $this->endDocument('atom');
805
806     }
807
808     function showRssGroups($group, $title, $link, $subtitle)
809     {
810
811         $this->initDocument('rss');
812
813         $this->element('title', null, $title);
814         $this->element('link', null, $link);
815         $this->element('description', null, $subtitle);
816         $this->element('language', null, 'en-us');
817         $this->element('ttl', null, '40');
818
819         if (is_array($group)) {
820             foreach ($group as $g) {
821                 $twitter_group = $this->twitterRssGroupArray($g);
822                 $this->showTwitterRssItem($twitter_group);
823             }
824         } else {
825             while ($group->fetch()) {
826                 $twitter_group = $this->twitterRssGroupArray($group);
827                 $this->showTwitterRssItem($twitter_group);
828             }
829         }
830
831         $this->endTwitterRss();
832     }
833
834     function showTwitterAtomEntry($entry)
835     {
836         $this->elementStart('entry');
837         $this->element('title', null, common_xml_safe_str($entry['title']));
838         $this->element(
839             'content',
840             array('type' => 'html'),
841             common_xml_safe_str($entry['content'])
842         );
843         $this->element('id', null, $entry['id']);
844         $this->element('published', null, $entry['published']);
845         $this->element('updated', null, $entry['updated']);
846         $this->element('link', array('type' => 'text/html',
847                                      'href' => $entry['link'],
848                                      'rel' => 'alternate'));
849         $this->element('link', array('type' => $entry['avatar-type'],
850                                      'href' => $entry['avatar'],
851                                      'rel' => 'image'));
852         $this->elementStart('author');
853
854         $this->element('name', null, $entry['author-name']);
855         $this->element('uri', null, $entry['author-uri']);
856
857         $this->elementEnd('author');
858         $this->elementEnd('entry');
859     }
860
861     function showXmlDirectMessage($dm)
862     {
863         $this->elementStart('direct_message');
864         foreach($dm as $element => $value) {
865             switch ($element) {
866             case 'sender':
867             case 'recipient':
868                 $this->showTwitterXmlUser($value, $element);
869                 break;
870             case 'text':
871                 $this->element($element, null, common_xml_safe_str($value));
872                 break;
873             default:
874                 $this->element($element, null, $value);
875                 break;
876             }
877         }
878         $this->elementEnd('direct_message');
879     }
880
881     function directMessageArray($message)
882     {
883         $dmsg = array();
884
885         $from_profile = $message->getFrom();
886         $to_profile = $message->getTo();
887
888         $dmsg['id'] = $message->id;
889         $dmsg['sender_id'] = $message->from_profile;
890         $dmsg['text'] = trim($message->content);
891         $dmsg['recipient_id'] = $message->to_profile;
892         $dmsg['created_at'] = $this->dateTwitter($message->created);
893         $dmsg['sender_screen_name'] = $from_profile->nickname;
894         $dmsg['recipient_screen_name'] = $to_profile->nickname;
895         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
896         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
897
898         return $dmsg;
899     }
900
901     function rssDirectMessageArray($message)
902     {
903         $entry = array();
904
905         $from = $message->getFrom();
906
907         $entry['title'] = sprintf('Message from %1$s to %2$s',
908             $from->nickname, $message->getTo()->nickname);
909
910         $entry['content'] = common_xml_safe_str($message->rendered);
911         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
912         $entry['published'] = common_date_iso8601($message->created);
913
914         $taguribase = TagURI::base();
915
916         $entry['id'] = "tag:$taguribase:$entry[link]";
917         $entry['updated'] = $entry['published'];
918
919         $entry['author-name'] = $from->getBestName();
920         $entry['author-uri'] = $from->homepage;
921
922         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
923
924         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
925         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
926
927         // RSS item specific
928
929         $entry['description'] = $entry['content'];
930         $entry['pubDate'] = common_date_rfc2822($message->created);
931         $entry['guid'] = $entry['link'];
932
933         return $entry;
934     }
935
936     function showSingleXmlDirectMessage($message)
937     {
938         $this->initDocument('xml');
939         $dmsg = $this->directMessageArray($message);
940         $this->showXmlDirectMessage($dmsg);
941         $this->endDocument('xml');
942     }
943
944     function showSingleJsonDirectMessage($message)
945     {
946         $this->initDocument('json');
947         $dmsg = $this->directMessageArray($message);
948         $this->showJsonObjects($dmsg);
949         $this->endDocument('json');
950     }
951
952     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
953     {
954
955         $this->initDocument('atom');
956
957         $this->element('title', null, common_xml_safe_str($title));
958         $this->element('id', null, $id);
959         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
960
961         if (!is_null($selfuri)) {
962             $this->element('link', array('href' => $selfuri,
963                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
964         }
965
966         $this->element('updated', null, common_date_iso8601('now'));
967         $this->element('subtitle', null, common_xml_safe_str($subtitle));
968
969         if (is_array($group)) {
970             foreach ($group as $g) {
971                 $this->raw($g->asAtomEntry());
972             }
973         } else {
974             while ($group->fetch()) {
975                 $this->raw($group->asAtomEntry());
976             }
977         }
978
979         $this->endDocument('atom');
980
981     }
982
983     function showJsonTimeline($notice)
984     {
985
986         $this->initDocument('json');
987
988         $statuses = array();
989
990         if (is_array($notice)) {
991             foreach ($notice as $n) {
992                 $twitter_status = $this->twitterStatusArray($n);
993                 array_push($statuses, $twitter_status);
994             }
995         } else {
996             while ($notice->fetch()) {
997                 $twitter_status = $this->twitterStatusArray($notice);
998                 array_push($statuses, $twitter_status);
999             }
1000         }
1001
1002         $this->showJsonObjects($statuses);
1003
1004         $this->endDocument('json');
1005     }
1006
1007     function showJsonGroups($group)
1008     {
1009
1010         $this->initDocument('json');
1011
1012         $groups = array();
1013
1014         if (is_array($group)) {
1015             foreach ($group as $g) {
1016                 $twitter_group = $this->twitterGroupArray($g);
1017                 array_push($groups, $twitter_group);
1018             }
1019         } else {
1020             while ($group->fetch()) {
1021                 $twitter_group = $this->twitterGroupArray($group);
1022                 array_push($groups, $twitter_group);
1023             }
1024         }
1025
1026         $this->showJsonObjects($groups);
1027
1028         $this->endDocument('json');
1029     }
1030
1031     function showXmlGroups($group)
1032     {
1033
1034         $this->initDocument('xml');
1035         $this->elementStart('groups', array('type' => 'array'));
1036
1037         if (is_array($group)) {
1038             foreach ($group as $g) {
1039                 $twitter_group = $this->twitterGroupArray($g);
1040                 $this->showTwitterXmlGroup($twitter_group);
1041             }
1042         } else {
1043             while ($group->fetch()) {
1044                 $twitter_group = $this->twitterGroupArray($group);
1045                 $this->showTwitterXmlGroup($twitter_group);
1046             }
1047         }
1048
1049         $this->elementEnd('groups');
1050         $this->endDocument('xml');
1051     }
1052
1053     function showTwitterXmlUsers($user)
1054     {
1055
1056         $this->initDocument('xml');
1057         $this->elementStart('users', array('type' => 'array'));
1058
1059         if (is_array($user)) {
1060             foreach ($user as $u) {
1061                 $twitter_user = $this->twitterUserArray($u);
1062                 $this->showTwitterXmlUser($twitter_user);
1063             }
1064         } else {
1065             while ($user->fetch()) {
1066                 $twitter_user = $this->twitterUserArray($user);
1067                 $this->showTwitterXmlUser($twitter_user);
1068             }
1069         }
1070
1071         $this->elementEnd('users');
1072         $this->endDocument('xml');
1073     }
1074
1075     function showJsonUsers($user)
1076     {
1077
1078         $this->initDocument('json');
1079
1080         $users = array();
1081
1082         if (is_array($user)) {
1083             foreach ($user as $u) {
1084                 $twitter_user = $this->twitterUserArray($u);
1085                 array_push($users, $twitter_user);
1086             }
1087         } else {
1088             while ($user->fetch()) {
1089                 $twitter_user = $this->twitterUserArray($user);
1090                 array_push($users, $twitter_user);
1091             }
1092         }
1093
1094         $this->showJsonObjects($users);
1095
1096         $this->endDocument('json');
1097     }
1098
1099     function showSingleJsonGroup($group)
1100     {
1101         $this->initDocument('json');
1102         $twitter_group = $this->twitterGroupArray($group);
1103         $this->showJsonObjects($twitter_group);
1104         $this->endDocument('json');
1105     }
1106
1107     function showSingleXmlGroup($group)
1108     {
1109         $this->initDocument('xml');
1110         $twitter_group = $this->twitterGroupArray($group);
1111         $this->showTwitterXmlGroup($twitter_group);
1112         $this->endDocument('xml');
1113     }
1114
1115     function dateTwitter($dt)
1116     {
1117         $dateStr = date('d F Y H:i:s', strtotime($dt));
1118         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1119         $d->setTimezone(new DateTimeZone(common_timezone()));
1120         return $d->format('D M d H:i:s O Y');
1121     }
1122
1123     function initDocument($type='xml')
1124     {
1125         switch ($type) {
1126         case 'xml':
1127             header('Content-Type: application/xml; charset=utf-8');
1128             $this->startXML();
1129             break;
1130         case 'json':
1131             header('Content-Type: application/json; charset=utf-8');
1132
1133             // Check for JSONP callback
1134             $callback = $this->arg('callback');
1135             if ($callback) {
1136                 print $callback . '(';
1137             }
1138             break;
1139         case 'rss':
1140             header("Content-Type: application/rss+xml; charset=utf-8");
1141             $this->initTwitterRss();
1142             break;
1143         case 'atom':
1144             header('Content-Type: application/atom+xml; charset=utf-8');
1145             $this->initTwitterAtom();
1146             break;
1147         default:
1148             // TRANS: Client error on an API request with an unsupported data format.
1149             $this->clientError(_('Not a supported data format.'));
1150             break;
1151         }
1152
1153         return;
1154     }
1155
1156     function endDocument($type='xml')
1157     {
1158         switch ($type) {
1159         case 'xml':
1160             $this->endXML();
1161             break;
1162         case 'json':
1163
1164             // Check for JSONP callback
1165             $callback = $this->arg('callback');
1166             if ($callback) {
1167                 print ')';
1168             }
1169             break;
1170         case 'rss':
1171             $this->endTwitterRss();
1172             break;
1173         case 'atom':
1174             $this->endTwitterRss();
1175             break;
1176         default:
1177             // TRANS: Client error on an API request with an unsupported data format.
1178             $this->clientError(_('Not a supported data format.'));
1179             break;
1180         }
1181         return;
1182     }
1183
1184     function clientError($msg, $code = 400, $format = 'xml')
1185     {
1186         $action = $this->trimmed('action');
1187
1188         common_debug("User error '$code' on '$action': $msg", __FILE__);
1189
1190         if (!array_key_exists($code, ClientErrorAction::$status)) {
1191             $code = 400;
1192         }
1193
1194         $status_string = ClientErrorAction::$status[$code];
1195
1196         header('HTTP/1.1 '.$code.' '.$status_string);
1197
1198         if ($format == 'xml') {
1199             $this->initDocument('xml');
1200             $this->elementStart('hash');
1201             $this->element('error', null, $msg);
1202             $this->element('request', null, $_SERVER['REQUEST_URI']);
1203             $this->elementEnd('hash');
1204             $this->endDocument('xml');
1205         } elseif ($format == 'json'){
1206             $this->initDocument('json');
1207             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1208             print(json_encode($error_array));
1209             $this->endDocument('json');
1210         } else {
1211
1212             // If user didn't request a useful format, throw a regular client error
1213             throw new ClientException($msg, $code);
1214         }
1215     }
1216
1217     function serverError($msg, $code = 500, $content_type = 'xml')
1218     {
1219         $action = $this->trimmed('action');
1220
1221         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1222
1223         if (!array_key_exists($code, ServerErrorAction::$status)) {
1224             $code = 400;
1225         }
1226
1227         $status_string = ServerErrorAction::$status[$code];
1228
1229         header('HTTP/1.1 '.$code.' '.$status_string);
1230
1231         if ($content_type == 'xml') {
1232             $this->initDocument('xml');
1233             $this->elementStart('hash');
1234             $this->element('error', null, $msg);
1235             $this->element('request', null, $_SERVER['REQUEST_URI']);
1236             $this->elementEnd('hash');
1237             $this->endDocument('xml');
1238         } else {
1239             $this->initDocument('json');
1240             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1241             print(json_encode($error_array));
1242             $this->endDocument('json');
1243         }
1244     }
1245
1246     function initTwitterRss()
1247     {
1248         $this->startXML();
1249         $this->elementStart(
1250             'rss',
1251             array(
1252                 'version'      => '2.0',
1253                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1254                 'xmlns:georss' => 'http://www.georss.org/georss'
1255             )
1256         );
1257         $this->elementStart('channel');
1258         Event::handle('StartApiRss', array($this));
1259     }
1260
1261     function endTwitterRss()
1262     {
1263         $this->elementEnd('channel');
1264         $this->elementEnd('rss');
1265         $this->endXML();
1266     }
1267
1268     function initTwitterAtom()
1269     {
1270         $this->startXML();
1271         // FIXME: don't hardcode the language here!
1272         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1273                                           'xml:lang' => 'en-US',
1274                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1275     }
1276
1277     function endTwitterAtom()
1278     {
1279         $this->elementEnd('feed');
1280         $this->endXML();
1281     }
1282
1283     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1284     {
1285         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1286         switch ($content_type) {
1287         case 'xml':
1288             $this->showTwitterXmlUser($profile_array);
1289             break;
1290         case 'json':
1291             $this->showJsonObjects($profile_array);
1292             break;
1293         default:
1294             // TRANS: Client error on an API request with an unsupported data format.
1295             $this->clientError(_('Not a supported data format.'));
1296             return;
1297         }
1298         return;
1299     }
1300
1301     function getTargetUser($id)
1302     {
1303         if (empty($id)) {
1304
1305             // Twitter supports these other ways of passing the user ID
1306             if (is_numeric($this->arg('id'))) {
1307                 return User::staticGet($this->arg('id'));
1308             } else if ($this->arg('id')) {
1309                 $nickname = common_canonical_nickname($this->arg('id'));
1310                 return User::staticGet('nickname', $nickname);
1311             } else if ($this->arg('user_id')) {
1312                 // This is to ensure that a non-numeric user_id still
1313                 // overrides screen_name even if it doesn't get used
1314                 if (is_numeric($this->arg('user_id'))) {
1315                     return User::staticGet('id', $this->arg('user_id'));
1316                 }
1317             } else if ($this->arg('screen_name')) {
1318                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1319                 return User::staticGet('nickname', $nickname);
1320             } else {
1321                 // Fall back to trying the currently authenticated user
1322                 return $this->auth_user;
1323             }
1324
1325         } else if (is_numeric($id)) {
1326             return User::staticGet($id);
1327         } else {
1328             $nickname = common_canonical_nickname($id);
1329             return User::staticGet('nickname', $nickname);
1330         }
1331     }
1332
1333     function getTargetGroup($id)
1334     {
1335         if (empty($id)) {
1336             if (is_numeric($this->arg('id'))) {
1337                 return User_group::staticGet($this->arg('id'));
1338             } else if ($this->arg('id')) {
1339                 $nickname = common_canonical_nickname($this->arg('id'));
1340                 $local = Local_group::staticGet('nickname', $nickname);
1341                 if (empty($local)) {
1342                     return null;
1343                 } else {
1344                     return User_group::staticGet('id', $local->id);
1345                 }
1346             } else if ($this->arg('group_id')) {
1347                 // This is to ensure that a non-numeric user_id still
1348                 // overrides screen_name even if it doesn't get used
1349                 if (is_numeric($this->arg('group_id'))) {
1350                     return User_group::staticGet('id', $this->arg('group_id'));
1351                 }
1352             } else if ($this->arg('group_name')) {
1353                 $nickname = common_canonical_nickname($this->arg('group_name'));
1354                 $local = Local_group::staticGet('nickname', $nickname);
1355                 if (empty($local)) {
1356                     return null;
1357                 } else {
1358                     return User_group::staticGet('id', $local->group_id);
1359                 }
1360             }
1361
1362         } else if (is_numeric($id)) {
1363             return User_group::staticGet($id);
1364         } else {
1365             $nickname = common_canonical_nickname($id);
1366             $local = Local_group::staticGet('nickname', $nickname);
1367             if (empty($local)) {
1368                 return null;
1369             } else {
1370                 return User_group::staticGet('id', $local->group_id);
1371             }
1372         }
1373     }
1374
1375     /**
1376      * Returns query argument or default value if not found. Certain
1377      * parameters used throughout the API are lightly scrubbed and
1378      * bounds checked.  This overrides Action::arg().
1379      *
1380      * @param string $key requested argument
1381      * @param string $def default value to return if $key is not provided
1382      *
1383      * @return var $var
1384      */
1385     function arg($key, $def=null)
1386     {
1387
1388         // XXX: Do even more input validation/scrubbing?
1389
1390         if (array_key_exists($key, $this->args)) {
1391             switch($key) {
1392             case 'page':
1393                 $page = (int)$this->args['page'];
1394                 return ($page < 1) ? 1 : $page;
1395             case 'count':
1396                 $count = (int)$this->args['count'];
1397                 if ($count < 1) {
1398                     return 20;
1399                 } elseif ($count > 200) {
1400                     return 200;
1401                 } else {
1402                     return $count;
1403                 }
1404             case 'since_id':
1405                 $since_id = (int)$this->args['since_id'];
1406                 return ($since_id < 1) ? 0 : $since_id;
1407             case 'max_id':
1408                 $max_id = (int)$this->args['max_id'];
1409                 return ($max_id < 1) ? 0 : $max_id;
1410             default:
1411                 return parent::arg($key, $def);
1412             }
1413         } else {
1414             return $def;
1415         }
1416     }
1417
1418     /**
1419      * Calculate the complete URI that called up this action.  Used for
1420      * Atom rel="self" links.  Warning: this is funky.
1421      *
1422      * @return string URL    a URL suitable for rel="self" Atom links
1423      */
1424     function getSelfUri()
1425     {
1426         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1427
1428         $id = $this->arg('id');
1429         $aargs = array('format' => $this->format);
1430         if (!empty($id)) {
1431             $aargs['id'] = $id;
1432         }
1433
1434         $tag = $this->arg('tag');
1435         if (!empty($tag)) {
1436             $aargs['tag'] = $tag;
1437         }
1438
1439         parse_str($_SERVER['QUERY_STRING'], $params);
1440         $pstring = '';
1441         if (!empty($params)) {
1442             unset($params['p']);
1443             $pstring = http_build_query($params);
1444         }
1445
1446         $uri = common_local_url($action, $aargs);
1447
1448         if (!empty($pstring)) {
1449             $uri .= '?' . $pstring;
1450         }
1451
1452         return $uri;
1453     }
1454
1455 }