3 * StatusNet, the distributed open-source microblogging tool
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.
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.
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/>.
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/
35 /* External API usage documentation. Please update when you change how the API works. */
37 /*! @mainpage StatusNet REST API
41 Some explanatory text about the API would be nice.
45 @subsection timelinesmethods_sec Timeline Methods
47 @li @ref friendstimeline
49 @subsection statusmethods_sec Status Methods
51 @li @ref statusesupdate
53 @subsection usermethods_sec User Methods
55 @subsection directmessagemethods_sec Direct Message Methods
57 @subsection friendshipmethods_sec Friendship Methods
59 @subsection socialgraphmethods_sec Social Graph Methods
61 @subsection accountmethods_sec Account Methods
63 @subsection favoritesmethods_sec Favorites Methods
65 @subsection blockmethods_sec Block Methods
67 @subsection oauthmethods_sec OAuth Methods
69 @subsection helpmethods_sec Help Methods
71 @subsection groupmethods_sec Group Methods
73 @page apiroot API Root
75 The URLs for methods referred to in this API documentation are
76 relative to the StatusNet API root. The API root is determined by the
77 site's @b server and @b path variables, which are generally specified
78 in config.php. For example:
81 $config['site']['server'] = 'example.org';
82 $config['site']['path'] = 'statusnet'
85 The pattern for a site's API root is: @c protocol://server/path/api E.g:
87 @c http://example.org/statusnet/api
89 The @b path can be empty. In that case the API root would simply be:
91 @c http://example.org/api
95 if (!defined('STATUSNET')) {
100 * Contains most of the Twitter-compatible API output functions.
104 * @author Craig Andrews <candrews@integralblue.com>
105 * @author Dan Moore <dan@moore.cx>
106 * @author Evan Prodromou <evan@status.net>
107 * @author Jeffery To <jeffery.to@gmail.com>
108 * @author Toby Inkster <mail@tobyinkster.co.uk>
109 * @author Zach Copley <zach@status.net>
110 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
111 * @link http://status.net/
114 class ApiAction extends Action
117 const READ_WRITE = 2;
121 var $auth_user = null;
125 var $since_id = null;
127 var $access = self::READ_ONLY; // read (default) or read-write
132 * @param array $args Web and URL arguments
134 * @return boolean false if user doesn't exist
137 function prepare($args)
139 StatusNet::setApi(true); // reduce exception reports to aid in debugging
140 parent::prepare($args);
142 $this->format = $this->arg('format');
143 $this->page = (int)$this->arg('page', 1);
144 $this->count = (int)$this->arg('count', 20);
145 $this->max_id = (int)$this->arg('max_id', 0);
146 $this->since_id = (int)$this->arg('since_id', 0);
148 if ($this->arg('since')) {
149 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
158 * @param array $args Arguments from $_REQUEST
163 function handle($args)
165 header('Access-Control-Allow-Origin: *');
166 parent::handle($args);
170 * Overrides XMLOutputter::element to write booleans as strings (true|false).
171 * See that method's documentation for more info.
173 * @param string $tag Element type or tagname
174 * @param array $attrs Array of element attributes, as
176 * @param string $content string content of the element
180 function element($tag, $attrs=null, $content=null)
182 if (is_bool($content)) {
183 $content = ($content ? 'true' : 'false');
186 return parent::element($tag, $attrs, $content);
189 function twitterUserArray($profile, $get_notice=false)
191 $twitter_user = array();
193 $twitter_user['id'] = intval($profile->id);
194 $twitter_user['name'] = $profile->getBestName();
195 $twitter_user['screen_name'] = $profile->nickname;
196 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
197 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
199 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
200 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
201 Avatar::defaultImage(AVATAR_STREAM_SIZE);
203 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
204 $twitter_user['protected'] = false; # not supported by StatusNet yet
205 $twitter_user['followers_count'] = $profile->subscriberCount();
208 $user = $profile->getUser();
210 // Note: some profiles don't have an associated user
212 $defaultDesign = Design::siteDesign();
215 $design = $user->getDesign();
218 if (empty($design)) {
219 $design = $defaultDesign;
222 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
223 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
224 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
225 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
226 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
227 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
228 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
229 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
230 $twitter_user['profile_sidebar_border_color'] = '';
232 $twitter_user['friends_count'] = $profile->subscriptionCount();
234 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
236 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
240 if (!empty($user) && $user->timezone) {
241 $timezone = $user->timezone;
245 $t->setTimezone(new DateTimeZone($timezone));
247 $twitter_user['utc_offset'] = $t->format('Z');
248 $twitter_user['time_zone'] = $timezone;
250 $twitter_user['profile_background_image_url']
251 = empty($design->backgroundimage)
252 ? '' : ($design->disposition & BACKGROUND_ON)
253 ? Design::url($design->backgroundimage) : '';
255 $twitter_user['profile_background_tile']
256 = empty($design->disposition)
257 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
259 $twitter_user['statuses_count'] = $profile->noticeCount();
261 // Is the requesting user following this user?
262 $twitter_user['following'] = false;
263 $twitter_user['notifications'] = false;
265 if (isset($this->auth_user)) {
267 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
270 $sub = Subscription::pkeyGet(array('subscriber' =>
271 $this->auth_user->id,
272 'subscribed' => $profile->id));
275 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
280 $notice = $profile->getCurrentNotice();
283 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
287 return $twitter_user;
290 function twitterStatusArray($notice, $include_user=true)
292 $base = $this->twitterSimpleStatusArray($notice, $include_user);
294 if (!empty($notice->repeat_of)) {
295 $original = Notice::staticGet('id', $notice->repeat_of);
296 if (!empty($original)) {
297 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
298 $base['retweeted_status'] = $original_array;
305 function twitterSimpleStatusArray($notice, $include_user=true)
307 $profile = $notice->getProfile();
309 $twitter_status = array();
310 $twitter_status['text'] = $notice->content;
311 $twitter_status['truncated'] = false; # Not possible on StatusNet
312 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
313 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
314 intval($notice->reply_to) : null;
315 $twitter_status['source'] = $this->sourceLink($notice->source);
316 $twitter_status['id'] = intval($notice->id);
318 $replier_profile = null;
320 if ($notice->reply_to) {
321 $reply = Notice::staticGet(intval($notice->reply_to));
323 $replier_profile = $reply->getProfile();
327 $twitter_status['in_reply_to_user_id'] =
328 ($replier_profile) ? intval($replier_profile->id) : null;
329 $twitter_status['in_reply_to_screen_name'] =
330 ($replier_profile) ? $replier_profile->nickname : null;
332 if (isset($notice->lat) && isset($notice->lon)) {
333 // This is the format that GeoJSON expects stuff to be in
334 $twitter_status['geo'] = array('type' => 'Point',
335 'coordinates' => array((float) $notice->lat,
336 (float) $notice->lon));
338 $twitter_status['geo'] = null;
341 if (isset($this->auth_user)) {
342 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
344 $twitter_status['favorited'] = false;
348 $attachments = $notice->attachments();
350 if (!empty($attachments)) {
352 $twitter_status['attachments'] = array();
354 foreach ($attachments as $attachment) {
355 $enclosure_o=$attachment->getEnclosure();
357 $enclosure = array();
358 $enclosure['url'] = $enclosure_o->url;
359 $enclosure['mimetype'] = $enclosure_o->mimetype;
360 $enclosure['size'] = $enclosure_o->size;
361 $twitter_status['attachments'][] = $enclosure;
366 if ($include_user && $profile) {
367 # Don't get notice (recursive!)
368 $twitter_user = $this->twitterUserArray($profile, false);
369 $twitter_status['user'] = $twitter_user;
372 return $twitter_status;
375 function twitterGroupArray($group)
377 $twitter_group=array();
378 $twitter_group['id']=$group->id;
379 $twitter_group['url']=$group->permalink();
380 $twitter_group['nickname']=$group->nickname;
381 $twitter_group['fullname']=$group->fullname;
382 $twitter_group['original_logo']=$group->original_logo;
383 $twitter_group['homepage_logo']=$group->homepage_logo;
384 $twitter_group['stream_logo']=$group->stream_logo;
385 $twitter_group['mini_logo']=$group->mini_logo;
386 $twitter_group['homepage']=$group->homepage;
387 $twitter_group['description']=$group->description;
388 $twitter_group['location']=$group->location;
389 $twitter_group['created']=$this->dateTwitter($group->created);
390 $twitter_group['modified']=$this->dateTwitter($group->modified);
391 return $twitter_group;
394 function twitterRssGroupArray($group)
397 $entry['content']=$group->description;
398 $entry['title']=$group->nickname;
399 $entry['link']=$group->permalink();
400 $entry['published']=common_date_iso8601($group->created);
401 $entry['updated']==common_date_iso8601($group->modified);
402 $taguribase = common_config('integration', 'groupuri');
403 $entry['id'] = "group:$groupuribase:$entry[link]";
405 $entry['description'] = $entry['content'];
406 $entry['pubDate'] = common_date_rfc2822($group->created);
407 $entry['guid'] = $entry['link'];
412 function twitterRssEntryArray($notice)
414 $profile = $notice->getProfile();
417 // We trim() to avoid extraneous whitespace in the output
419 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
420 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
421 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
422 $entry['published'] = common_date_iso8601($notice->created);
424 $taguribase = TagURI::base();
425 $entry['id'] = "tag:$taguribase:$entry[link]";
427 $entry['updated'] = $entry['published'];
428 $entry['author'] = $profile->getBestName();
431 $attachments = $notice->attachments();
432 $enclosures = array();
434 foreach ($attachments as $attachment) {
435 $enclosure_o=$attachment->getEnclosure();
437 $enclosure = array();
438 $enclosure['url'] = $enclosure_o->url;
439 $enclosure['mimetype'] = $enclosure_o->mimetype;
440 $enclosure['size'] = $enclosure_o->size;
441 $enclosures[] = $enclosure;
445 if (!empty($enclosures)) {
446 $entry['enclosures'] = $enclosures;
450 $tag = new Notice_tag();
451 $tag->notice_id = $notice->id;
453 $entry['tags']=array();
454 while ($tag->fetch()) {
455 $entry['tags'][]=$tag->tag;
461 $entry['description'] = $entry['content'];
462 $entry['pubDate'] = common_date_rfc2822($notice->created);
463 $entry['guid'] = $entry['link'];
465 if (isset($notice->lat) && isset($notice->lon)) {
466 // This is the format that GeoJSON expects stuff to be in.
467 // showGeoRSS() below uses it for XML output, so we reuse it
468 $entry['geo'] = array('type' => 'Point',
469 'coordinates' => array((float) $notice->lat,
470 (float) $notice->lon));
472 $entry['geo'] = null;
478 function twitterRelationshipArray($source, $target)
480 $relationship = array();
482 $relationship['source'] =
483 $this->relationshipDetailsArray($source, $target);
484 $relationship['target'] =
485 $this->relationshipDetailsArray($target, $source);
487 return array('relationship' => $relationship);
490 function relationshipDetailsArray($source, $target)
494 $details['screen_name'] = $source->nickname;
495 $details['followed_by'] = $target->isSubscribed($source);
496 $details['following'] = $source->isSubscribed($target);
498 $notifications = false;
500 if ($source->isSubscribed($target)) {
502 $sub = Subscription::pkeyGet(array('subscriber' =>
503 $source->id, 'subscribed' => $target->id));
506 $notifications = ($sub->jabber || $sub->sms);
510 $details['notifications_enabled'] = $notifications;
511 $details['blocking'] = $source->hasBlocked($target);
512 $details['id'] = $source->id;
517 function showTwitterXmlRelationship($relationship)
519 $this->elementStart('relationship');
521 foreach($relationship as $element => $value) {
522 if ($element == 'source' || $element == 'target') {
523 $this->elementStart($element);
524 $this->showXmlRelationshipDetails($value);
525 $this->elementEnd($element);
529 $this->elementEnd('relationship');
532 function showXmlRelationshipDetails($details)
534 foreach($details as $element => $value) {
535 $this->element($element, null, $value);
539 function showTwitterXmlStatus($twitter_status, $tag='status')
541 $this->elementStart($tag);
542 foreach($twitter_status as $element => $value) {
545 $this->showTwitterXmlUser($twitter_status['user']);
548 $this->element($element, null, common_xml_safe_str($value));
551 $this->showXmlAttachments($twitter_status['attachments']);
554 $this->showGeoXML($value);
556 case 'retweeted_status':
557 $this->showTwitterXmlStatus($value, 'retweeted_status');
560 $this->element($element, null, $value);
563 $this->elementEnd($tag);
566 function showTwitterXmlGroup($twitter_group)
568 $this->elementStart('group');
569 foreach($twitter_group as $element => $value) {
570 $this->element($element, null, $value);
572 $this->elementEnd('group');
575 function showTwitterXmlUser($twitter_user, $role='user')
577 $this->elementStart($role);
578 foreach($twitter_user as $element => $value) {
579 if ($element == 'status') {
580 $this->showTwitterXmlStatus($twitter_user['status']);
582 $this->element($element, null, $value);
585 $this->elementEnd($role);
588 function showXmlAttachments($attachments) {
589 if (!empty($attachments)) {
590 $this->elementStart('attachments', array('type' => 'array'));
591 foreach ($attachments as $attachment) {
593 $attrs['url'] = $attachment['url'];
594 $attrs['mimetype'] = $attachment['mimetype'];
595 $attrs['size'] = $attachment['size'];
596 $this->element('enclosure', $attrs, '');
598 $this->elementEnd('attachments');
602 function showGeoXML($geo)
606 $this->element('geo');
608 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
609 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
610 $this->elementEnd('geo');
614 function showGeoRSS($geo)
620 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
625 function showTwitterRssItem($entry)
627 $this->elementStart('item');
628 $this->element('title', null, $entry['title']);
629 $this->element('description', null, $entry['description']);
630 $this->element('pubDate', null, $entry['pubDate']);
631 $this->element('guid', null, $entry['guid']);
632 $this->element('link', null, $entry['link']);
634 # RSS only supports 1 enclosure per item
635 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
636 $enclosure = $entry['enclosures'][0];
637 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
640 if(array_key_exists('tags', $entry)){
641 foreach($entry['tags'] as $tag){
642 $this->element('category', null,$tag);
646 $this->showGeoRSS($entry['geo']);
647 $this->elementEnd('item');
650 function showJsonObjects($objects)
652 print(json_encode($objects));
655 function showSingleXmlStatus($notice)
657 $this->initDocument('xml');
658 $twitter_status = $this->twitterStatusArray($notice);
659 $this->showTwitterXmlStatus($twitter_status);
660 $this->endDocument('xml');
663 function show_single_json_status($notice)
665 $this->initDocument('json');
666 $status = $this->twitterStatusArray($notice);
667 $this->showJsonObjects($status);
668 $this->endDocument('json');
671 function showXmlTimeline($notice)
674 $this->initDocument('xml');
675 $this->elementStart('statuses', array('type' => 'array'));
677 if (is_array($notice)) {
678 foreach ($notice as $n) {
679 $twitter_status = $this->twitterStatusArray($n);
680 $this->showTwitterXmlStatus($twitter_status);
683 while ($notice->fetch()) {
684 $twitter_status = $this->twitterStatusArray($notice);
685 $this->showTwitterXmlStatus($twitter_status);
689 $this->elementEnd('statuses');
690 $this->endDocument('xml');
693 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
696 $this->initDocument('rss');
698 $this->element('title', null, $title);
699 $this->element('link', null, $link);
701 if (!is_null($self)) {
705 'type' => 'application/rss+xml',
712 if (!is_null($suplink)) {
713 // For FriendFeed's SUP protocol
714 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
715 'rel' => 'http://api.friendfeed.com/2008/03#sup',
717 'type' => 'application/json'));
720 if (!is_null($logo)) {
721 $this->elementStart('image');
722 $this->element('link', null, $link);
723 $this->element('title', null, $title);
724 $this->element('url', null, $logo);
725 $this->elementEnd('image');
728 $this->element('description', null, $subtitle);
729 $this->element('language', null, 'en-us');
730 $this->element('ttl', null, '40');
732 if (is_array($notice)) {
733 foreach ($notice as $n) {
734 $entry = $this->twitterRssEntryArray($n);
735 $this->showTwitterRssItem($entry);
738 while ($notice->fetch()) {
739 $entry = $this->twitterRssEntryArray($notice);
740 $this->showTwitterRssItem($entry);
744 $this->endTwitterRss();
747 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
750 $this->initDocument('atom');
752 $this->element('title', null, $title);
753 $this->element('id', null, $id);
754 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
756 if (!is_null($logo)) {
757 $this->element('logo',null,$logo);
760 if (!is_null($suplink)) {
761 # For FriendFeed's SUP protocol
762 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
764 'type' => 'application/json'));
767 if (!is_null($selfuri)) {
768 $this->element('link', array('href' => $selfuri,
769 'rel' => 'self', 'type' => 'application/atom+xml'), null);
772 $this->element('updated', null, common_date_iso8601('now'));
773 $this->element('subtitle', null, $subtitle);
775 if (is_array($notice)) {
776 foreach ($notice as $n) {
777 $this->raw($n->asAtomEntry());
780 while ($notice->fetch()) {
781 $this->raw($notice->asAtomEntry());
785 $this->endDocument('atom');
789 function showRssGroups($group, $title, $link, $subtitle)
792 $this->initDocument('rss');
794 $this->element('title', null, $title);
795 $this->element('link', null, $link);
796 $this->element('description', null, $subtitle);
797 $this->element('language', null, 'en-us');
798 $this->element('ttl', null, '40');
800 if (is_array($group)) {
801 foreach ($group as $g) {
802 $twitter_group = $this->twitterRssGroupArray($g);
803 $this->showTwitterRssItem($twitter_group);
806 while ($group->fetch()) {
807 $twitter_group = $this->twitterRssGroupArray($group);
808 $this->showTwitterRssItem($twitter_group);
812 $this->endTwitterRss();
815 function showTwitterAtomEntry($entry)
817 $this->elementStart('entry');
818 $this->element('title', null, common_xml_safe_str($entry['title']));
821 array('type' => 'html'),
822 common_xml_safe_str($entry['content'])
824 $this->element('id', null, $entry['id']);
825 $this->element('published', null, $entry['published']);
826 $this->element('updated', null, $entry['updated']);
827 $this->element('link', array('type' => 'text/html',
828 'href' => $entry['link'],
829 'rel' => 'alternate'));
830 $this->element('link', array('type' => $entry['avatar-type'],
831 'href' => $entry['avatar'],
833 $this->elementStart('author');
835 $this->element('name', null, $entry['author-name']);
836 $this->element('uri', null, $entry['author-uri']);
838 $this->elementEnd('author');
839 $this->elementEnd('entry');
842 function showXmlDirectMessage($dm)
844 $this->elementStart('direct_message');
845 foreach($dm as $element => $value) {
849 $this->showTwitterXmlUser($value, $element);
852 $this->element($element, null, common_xml_safe_str($value));
855 $this->element($element, null, $value);
859 $this->elementEnd('direct_message');
862 function directMessageArray($message)
866 $from_profile = $message->getFrom();
867 $to_profile = $message->getTo();
869 $dmsg['id'] = $message->id;
870 $dmsg['sender_id'] = $message->from_profile;
871 $dmsg['text'] = trim($message->content);
872 $dmsg['recipient_id'] = $message->to_profile;
873 $dmsg['created_at'] = $this->dateTwitter($message->created);
874 $dmsg['sender_screen_name'] = $from_profile->nickname;
875 $dmsg['recipient_screen_name'] = $to_profile->nickname;
876 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
877 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
882 function rssDirectMessageArray($message)
886 $from = $message->getFrom();
888 $entry['title'] = sprintf('Message from %1$s to %2$s',
889 $from->nickname, $message->getTo()->nickname);
891 $entry['content'] = common_xml_safe_str($message->rendered);
892 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
893 $entry['published'] = common_date_iso8601($message->created);
895 $taguribase = TagURI::base();
897 $entry['id'] = "tag:$taguribase:$entry[link]";
898 $entry['updated'] = $entry['published'];
900 $entry['author-name'] = $from->getBestName();
901 $entry['author-uri'] = $from->homepage;
903 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
905 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
906 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
910 $entry['description'] = $entry['content'];
911 $entry['pubDate'] = common_date_rfc2822($message->created);
912 $entry['guid'] = $entry['link'];
917 function showSingleXmlDirectMessage($message)
919 $this->initDocument('xml');
920 $dmsg = $this->directMessageArray($message);
921 $this->showXmlDirectMessage($dmsg);
922 $this->endDocument('xml');
925 function showSingleJsonDirectMessage($message)
927 $this->initDocument('json');
928 $dmsg = $this->directMessageArray($message);
929 $this->showJsonObjects($dmsg);
930 $this->endDocument('json');
933 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
936 $this->initDocument('atom');
938 $this->element('title', null, common_xml_safe_str($title));
939 $this->element('id', null, $id);
940 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
942 if (!is_null($selfuri)) {
943 $this->element('link', array('href' => $selfuri,
944 'rel' => 'self', 'type' => 'application/atom+xml'), null);
947 $this->element('updated', null, common_date_iso8601('now'));
948 $this->element('subtitle', null, common_xml_safe_str($subtitle));
950 if (is_array($group)) {
951 foreach ($group as $g) {
952 $this->raw($g->asAtomEntry());
955 while ($group->fetch()) {
956 $this->raw($group->asAtomEntry());
960 $this->endDocument('atom');
964 function showJsonTimeline($notice)
967 $this->initDocument('json');
971 if (is_array($notice)) {
972 foreach ($notice as $n) {
973 $twitter_status = $this->twitterStatusArray($n);
974 array_push($statuses, $twitter_status);
977 while ($notice->fetch()) {
978 $twitter_status = $this->twitterStatusArray($notice);
979 array_push($statuses, $twitter_status);
983 $this->showJsonObjects($statuses);
985 $this->endDocument('json');
988 function showJsonGroups($group)
991 $this->initDocument('json');
995 if (is_array($group)) {
996 foreach ($group as $g) {
997 $twitter_group = $this->twitterGroupArray($g);
998 array_push($groups, $twitter_group);
1001 while ($group->fetch()) {
1002 $twitter_group = $this->twitterGroupArray($group);
1003 array_push($groups, $twitter_group);
1007 $this->showJsonObjects($groups);
1009 $this->endDocument('json');
1012 function showXmlGroups($group)
1015 $this->initDocument('xml');
1016 $this->elementStart('groups', array('type' => 'array'));
1018 if (is_array($group)) {
1019 foreach ($group as $g) {
1020 $twitter_group = $this->twitterGroupArray($g);
1021 $this->showTwitterXmlGroup($twitter_group);
1024 while ($group->fetch()) {
1025 $twitter_group = $this->twitterGroupArray($group);
1026 $this->showTwitterXmlGroup($twitter_group);
1030 $this->elementEnd('groups');
1031 $this->endDocument('xml');
1034 function showTwitterXmlUsers($user)
1037 $this->initDocument('xml');
1038 $this->elementStart('users', array('type' => 'array'));
1040 if (is_array($user)) {
1041 foreach ($user as $u) {
1042 $twitter_user = $this->twitterUserArray($u);
1043 $this->showTwitterXmlUser($twitter_user);
1046 while ($user->fetch()) {
1047 $twitter_user = $this->twitterUserArray($user);
1048 $this->showTwitterXmlUser($twitter_user);
1052 $this->elementEnd('users');
1053 $this->endDocument('xml');
1056 function showJsonUsers($user)
1059 $this->initDocument('json');
1063 if (is_array($user)) {
1064 foreach ($user as $u) {
1065 $twitter_user = $this->twitterUserArray($u);
1066 array_push($users, $twitter_user);
1069 while ($user->fetch()) {
1070 $twitter_user = $this->twitterUserArray($user);
1071 array_push($users, $twitter_user);
1075 $this->showJsonObjects($users);
1077 $this->endDocument('json');
1080 function showSingleJsonGroup($group)
1082 $this->initDocument('json');
1083 $twitter_group = $this->twitterGroupArray($group);
1084 $this->showJsonObjects($twitter_group);
1085 $this->endDocument('json');
1088 function showSingleXmlGroup($group)
1090 $this->initDocument('xml');
1091 $twitter_group = $this->twitterGroupArray($group);
1092 $this->showTwitterXmlGroup($twitter_group);
1093 $this->endDocument('xml');
1096 function dateTwitter($dt)
1098 $dateStr = date('d F Y H:i:s', strtotime($dt));
1099 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1100 $d->setTimezone(new DateTimeZone(common_timezone()));
1101 return $d->format('D M d H:i:s O Y');
1104 function initDocument($type='xml')
1108 header('Content-Type: application/xml; charset=utf-8');
1112 header('Content-Type: application/json; charset=utf-8');
1114 // Check for JSONP callback
1115 $callback = $this->arg('callback');
1117 print $callback . '(';
1121 header("Content-Type: application/rss+xml; charset=utf-8");
1122 $this->initTwitterRss();
1125 header('Content-Type: application/atom+xml; charset=utf-8');
1126 $this->initTwitterAtom();
1129 // TRANS: Client error on an API request with an unsupported data format.
1130 $this->clientError(_('Not a supported data format.'));
1137 function endDocument($type='xml')
1145 // Check for JSONP callback
1146 $callback = $this->arg('callback');
1152 $this->endTwitterRss();
1155 $this->endTwitterRss();
1158 // TRANS: Client error on an API request with an unsupported data format.
1159 $this->clientError(_('Not a supported data format.'));
1165 function clientError($msg, $code = 400, $format = 'xml')
1167 $action = $this->trimmed('action');
1169 common_debug("User error '$code' on '$action': $msg", __FILE__);
1171 if (!array_key_exists($code, ClientErrorAction::$status)) {
1175 $status_string = ClientErrorAction::$status[$code];
1177 header('HTTP/1.1 '.$code.' '.$status_string);
1179 if ($format == 'xml') {
1180 $this->initDocument('xml');
1181 $this->elementStart('hash');
1182 $this->element('error', null, $msg);
1183 $this->element('request', null, $_SERVER['REQUEST_URI']);
1184 $this->elementEnd('hash');
1185 $this->endDocument('xml');
1186 } elseif ($format == 'json'){
1187 $this->initDocument('json');
1188 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1189 print(json_encode($error_array));
1190 $this->endDocument('json');
1193 // If user didn't request a useful format, throw a regular client error
1194 throw new ClientException($msg, $code);
1198 function serverError($msg, $code = 500, $content_type = 'xml')
1200 $action = $this->trimmed('action');
1202 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1204 if (!array_key_exists($code, ServerErrorAction::$status)) {
1208 $status_string = ServerErrorAction::$status[$code];
1210 header('HTTP/1.1 '.$code.' '.$status_string);
1212 if ($content_type == 'xml') {
1213 $this->initDocument('xml');
1214 $this->elementStart('hash');
1215 $this->element('error', null, $msg);
1216 $this->element('request', null, $_SERVER['REQUEST_URI']);
1217 $this->elementEnd('hash');
1218 $this->endDocument('xml');
1220 $this->initDocument('json');
1221 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1222 print(json_encode($error_array));
1223 $this->endDocument('json');
1227 function initTwitterRss()
1230 $this->elementStart(
1234 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1235 'xmlns:georss' => 'http://www.georss.org/georss'
1238 $this->elementStart('channel');
1239 Event::handle('StartApiRss', array($this));
1242 function endTwitterRss()
1244 $this->elementEnd('channel');
1245 $this->elementEnd('rss');
1249 function initTwitterAtom()
1252 // FIXME: don't hardcode the language here!
1253 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1254 'xml:lang' => 'en-US',
1255 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1258 function endTwitterAtom()
1260 $this->elementEnd('feed');
1264 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1266 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1267 switch ($content_type) {
1269 $this->showTwitterXmlUser($profile_array);
1272 $this->showJsonObjects($profile_array);
1275 // TRANS: Client error on an API request with an unsupported data format.
1276 $this->clientError(_('Not a supported data format.'));
1282 function getTargetUser($id)
1286 // Twitter supports these other ways of passing the user ID
1287 if (is_numeric($this->arg('id'))) {
1288 return User::staticGet($this->arg('id'));
1289 } else if ($this->arg('id')) {
1290 $nickname = common_canonical_nickname($this->arg('id'));
1291 return User::staticGet('nickname', $nickname);
1292 } else if ($this->arg('user_id')) {
1293 // This is to ensure that a non-numeric user_id still
1294 // overrides screen_name even if it doesn't get used
1295 if (is_numeric($this->arg('user_id'))) {
1296 return User::staticGet('id', $this->arg('user_id'));
1298 } else if ($this->arg('screen_name')) {
1299 $nickname = common_canonical_nickname($this->arg('screen_name'));
1300 return User::staticGet('nickname', $nickname);
1302 // Fall back to trying the currently authenticated user
1303 return $this->auth_user;
1306 } else if (is_numeric($id)) {
1307 return User::staticGet($id);
1309 $nickname = common_canonical_nickname($id);
1310 return User::staticGet('nickname', $nickname);
1314 function getTargetGroup($id)
1317 if (is_numeric($this->arg('id'))) {
1318 return User_group::staticGet($this->arg('id'));
1319 } else if ($this->arg('id')) {
1320 $nickname = common_canonical_nickname($this->arg('id'));
1321 $local = Local_group::staticGet('nickname', $nickname);
1322 if (empty($local)) {
1325 return User_group::staticGet('id', $local->id);
1327 } else if ($this->arg('group_id')) {
1328 // This is to ensure that a non-numeric user_id still
1329 // overrides screen_name even if it doesn't get used
1330 if (is_numeric($this->arg('group_id'))) {
1331 return User_group::staticGet('id', $this->arg('group_id'));
1333 } else if ($this->arg('group_name')) {
1334 $nickname = common_canonical_nickname($this->arg('group_name'));
1335 $local = Local_group::staticGet('nickname', $nickname);
1336 if (empty($local)) {
1339 return User_group::staticGet('id', $local->group_id);
1343 } else if (is_numeric($id)) {
1344 return User_group::staticGet($id);
1346 $nickname = common_canonical_nickname($id);
1347 $local = Local_group::staticGet('nickname', $nickname);
1348 if (empty($local)) {
1351 return User_group::staticGet('id', $local->group_id);
1356 function sourceLink($source)
1358 $source_name = _($source);
1371 $ns = Notice_source::staticGet($source);
1377 $app = Oauth_application::staticGet('name', $source);
1380 $url = $app->source_url;
1384 if (!empty($name) && !empty($url)) {
1385 $source_name = '<a href="' . $url . '">' . $name . '</a>';
1390 return $source_name;
1394 * Returns query argument or default value if not found. Certain
1395 * parameters used throughout the API are lightly scrubbed and
1396 * bounds checked. This overrides Action::arg().
1398 * @param string $key requested argument
1399 * @param string $def default value to return if $key is not provided
1403 function arg($key, $def=null)
1406 // XXX: Do even more input validation/scrubbing?
1408 if (array_key_exists($key, $this->args)) {
1411 $page = (int)$this->args['page'];
1412 return ($page < 1) ? 1 : $page;
1414 $count = (int)$this->args['count'];
1417 } elseif ($count > 200) {
1423 $since_id = (int)$this->args['since_id'];
1424 return ($since_id < 1) ? 0 : $since_id;
1426 $max_id = (int)$this->args['max_id'];
1427 return ($max_id < 1) ? 0 : $max_id;
1429 return parent::arg($key, $def);
1437 * Calculate the complete URI that called up this action. Used for
1438 * Atom rel="self" links. Warning: this is funky.
1440 * @return string URL a URL suitable for rel="self" Atom links
1442 function getSelfUri()
1444 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1446 $id = $this->arg('id');
1447 $aargs = array('format' => $this->format);
1452 $tag = $this->arg('tag');
1454 $aargs['tag'] = $tag;
1457 parse_str($_SERVER['QUERY_STRING'], $params);
1459 if (!empty($params)) {
1460 unset($params['p']);
1461 $pstring = http_build_query($params);
1464 $uri = common_local_url($action, $aargs);
1466 if (!empty($pstring)) {
1467 $uri .= '?' . $pstring;