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