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-2010 StatusNet, Inc.
31 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33 * @link http://status.net/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
77 The URLs for methods referred to in this API documentation are
78 relative to the StatusNet API root. The API root is determined by the
79 site's @b server and @b path variables, which are generally specified
80 in config.php. For example:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
108 * @author Craig Andrews <candrews@integralblue.com>
109 * @author Dan Moore <dan@moore.cx>
110 * @author Evan Prodromou <evan@status.net>
111 * @author Jeffery To <jeffery.to@gmail.com>
112 * @author Toby Inkster <mail@tobyinkster.co.uk>
113 * @author Zach Copley <zach@status.net>
114 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115 * @link http://status.net/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
124 var $auth_user = null;
128 var $since_id = null;
130 var $callback = null;
132 var $access = self::READ_ONLY; // read (default) or read-write
134 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
139 * @param array $args Web and URL arguments
141 * @return boolean false if user doesn't exist
143 function prepare($args)
145 StatusNet::setApi(true); // reduce exception reports to aid in debugging
146 parent::prepare($args);
148 $this->format = $this->arg('format');
149 $this->callback = $this->arg('callback');
150 $this->page = (int)$this->arg('page', 1);
151 $this->count = (int)$this->arg('count', 20);
152 $this->max_id = (int)$this->arg('max_id', 0);
153 $this->since_id = (int)$this->arg('since_id', 0);
155 if ($this->arg('since')) {
156 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
159 $this->source = $this->trimmed('source');
161 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
162 $this->source = 'api';
171 * @param array $args Arguments from $_REQUEST
175 function handle($args)
177 header('Access-Control-Allow-Origin: *');
178 parent::handle($args);
182 * Overrides XMLOutputter::element to write booleans as strings (true|false).
183 * See that method's documentation for more info.
185 * @param string $tag Element type or tagname
186 * @param array $attrs Array of element attributes, as
188 * @param string $content string content of the element
192 function element($tag, $attrs=null, $content=null)
194 if (is_bool($content)) {
195 $content = ($content ? 'true' : 'false');
198 return parent::element($tag, $attrs, $content);
201 function twitterUserArray($profile, $get_notice=false)
203 $twitter_user = array();
205 $user = $profile->getUser();
207 $twitter_user['id'] = intval($profile->id);
208 $twitter_user['name'] = $profile->getBestName();
209 $twitter_user['screen_name'] = $profile->nickname;
210 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
211 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
213 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
214 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
215 Avatar::defaultImage(AVATAR_STREAM_SIZE);
217 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
218 $twitter_user['protected'] = ($user->private_stream) ? true : false;
219 $twitter_user['followers_count'] = $profile->subscriberCount();
223 // Note: some profiles don't have an associated user
225 $defaultDesign = Design::siteDesign();
228 $design = $user->getDesign();
231 if (empty($design)) {
232 $design = $defaultDesign;
235 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
236 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
238 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
240 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
242 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243 $twitter_user['profile_sidebar_border_color'] = '';
245 $twitter_user['friends_count'] = $profile->subscriptionCount();
247 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
249 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
253 if (!empty($user) && $user->timezone) {
254 $timezone = $user->timezone;
258 $t->setTimezone(new DateTimeZone($timezone));
260 $twitter_user['utc_offset'] = $t->format('Z');
261 $twitter_user['time_zone'] = $timezone;
263 $twitter_user['profile_background_image_url']
264 = empty($design->backgroundimage)
265 ? '' : ($design->disposition & BACKGROUND_ON)
266 ? Design::url($design->backgroundimage) : '';
268 $twitter_user['profile_background_tile']
269 = (bool)($design->disposition & BACKGROUND_TILE);
271 $twitter_user['statuses_count'] = $profile->noticeCount();
273 // Is the requesting user following this user?
274 $twitter_user['following'] = false;
275 $twitter_user['statusnet:blocking'] = false;
276 $twitter_user['notifications'] = false;
278 if (isset($this->auth_user)) {
280 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
281 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
284 $sub = Subscription::pkeyGet(array('subscriber' =>
285 $this->auth_user->id,
286 'subscribed' => $profile->id));
289 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
294 $notice = $profile->getCurrentNotice();
297 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
301 // StatusNet-specific
303 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
305 return $twitter_user;
308 function twitterStatusArray($notice, $include_user=true)
310 $base = $this->twitterSimpleStatusArray($notice, $include_user);
312 if (!empty($notice->repeat_of)) {
313 $original = Notice::staticGet('id', $notice->repeat_of);
314 if (!empty($original)) {
315 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
316 $base['retweeted_status'] = $original_array;
323 function twitterSimpleStatusArray($notice, $include_user=true)
325 $profile = $notice->getProfile();
327 $twitter_status = array();
328 $twitter_status['text'] = $notice->content;
329 $twitter_status['truncated'] = false; # Not possible on StatusNet
330 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
331 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
332 intval($notice->reply_to) : null;
336 $ns = $notice->getSource();
338 if (!empty($ns->name) && !empty($ns->url)) {
339 $source = '<a href="'
340 . htmlspecialchars($ns->url)
341 . '" rel="nofollow">'
342 . htmlspecialchars($ns->name)
349 $twitter_status['source'] = $source;
350 $twitter_status['id'] = intval($notice->id);
352 $replier_profile = null;
354 if ($notice->reply_to) {
355 $reply = Notice::staticGet(intval($notice->reply_to));
357 $replier_profile = $reply->getProfile();
361 $twitter_status['in_reply_to_user_id'] =
362 ($replier_profile) ? intval($replier_profile->id) : null;
363 $twitter_status['in_reply_to_screen_name'] =
364 ($replier_profile) ? $replier_profile->nickname : null;
366 if (isset($notice->lat) && isset($notice->lon)) {
367 // This is the format that GeoJSON expects stuff to be in
368 $twitter_status['geo'] = array('type' => 'Point',
369 'coordinates' => array((float) $notice->lat,
370 (float) $notice->lon));
372 $twitter_status['geo'] = null;
375 if (isset($this->auth_user)) {
376 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
378 $twitter_status['favorited'] = false;
382 $attachments = $notice->attachments();
384 if (!empty($attachments)) {
386 $twitter_status['attachments'] = array();
388 foreach ($attachments as $attachment) {
389 $enclosure_o=$attachment->getEnclosure();
391 $enclosure = array();
392 $enclosure['url'] = $enclosure_o->url;
393 $enclosure['mimetype'] = $enclosure_o->mimetype;
394 $enclosure['size'] = $enclosure_o->size;
395 $twitter_status['attachments'][] = $enclosure;
400 if ($include_user && $profile) {
401 // Don't get notice (recursive!)
402 $twitter_user = $this->twitterUserArray($profile, false);
403 $twitter_status['user'] = $twitter_user;
406 // StatusNet-specific
408 $twitter_status['statusnet_html'] = $notice->rendered;
410 return $twitter_status;
413 function twitterGroupArray($group)
415 $twitter_group = array();
417 $twitter_group['id'] = intval($group->id);
418 $twitter_group['url'] = $group->permalink();
419 $twitter_group['nickname'] = $group->nickname;
420 $twitter_group['fullname'] = $group->fullname;
422 if (isset($this->auth_user)) {
423 $twitter_group['member'] = $this->auth_user->isMember($group);
424 $twitter_group['blocked'] = Group_block::isBlocked(
426 $this->auth_user->getProfile()
430 $twitter_group['member_count'] = $group->getMemberCount();
431 $twitter_group['original_logo'] = $group->original_logo;
432 $twitter_group['homepage_logo'] = $group->homepage_logo;
433 $twitter_group['stream_logo'] = $group->stream_logo;
434 $twitter_group['mini_logo'] = $group->mini_logo;
435 $twitter_group['homepage'] = $group->homepage;
436 $twitter_group['description'] = $group->description;
437 $twitter_group['location'] = $group->location;
438 $twitter_group['created'] = $this->dateTwitter($group->created);
439 $twitter_group['modified'] = $this->dateTwitter($group->modified);
441 return $twitter_group;
444 function twitterRssGroupArray($group)
447 $entry['content']=$group->description;
448 $entry['title']=$group->nickname;
449 $entry['link']=$group->permalink();
450 $entry['published']=common_date_iso8601($group->created);
451 $entry['updated']==common_date_iso8601($group->modified);
452 $taguribase = common_config('integration', 'groupuri');
453 $entry['id'] = "group:$groupuribase:$entry[link]";
455 $entry['description'] = $entry['content'];
456 $entry['pubDate'] = common_date_rfc2822($group->created);
457 $entry['guid'] = $entry['link'];
462 function twitterListArray($list)
464 $profile = Profile::staticGet('id', $list->tagger);
466 $twitter_list = array();
467 $twitter_list['id'] = $list->id;
468 $twitter_list['name'] = $list->tag;
469 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
470 $twitter_list['slug'] = $list->tag;
471 $twitter_list['description'] = $list->description;
472 $twitter_list['subscriber_count'] = $list->subscriberCount();
473 $twitter_list['member_count'] = $list->taggedCount();
474 $twitter_list['uri'] = $list->getUri();
476 if (isset($this->auth_user)) {
477 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
479 $twitter_list['following'] = false;
482 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
483 $twitter_list['user'] = $this->twitterUserArray($profile, false);
485 return $twitter_list;
488 function twitterRssEntryArray($notice)
492 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
493 $profile = $notice->getProfile();
495 // We trim() to avoid extraneous whitespace in the output
497 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
498 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
499 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
500 $entry['published'] = common_date_iso8601($notice->created);
502 $taguribase = TagURI::base();
503 $entry['id'] = "tag:$taguribase:$entry[link]";
505 $entry['updated'] = $entry['published'];
506 $entry['author'] = $profile->getBestName();
509 $attachments = $notice->attachments();
510 $enclosures = array();
512 foreach ($attachments as $attachment) {
513 $enclosure_o=$attachment->getEnclosure();
515 $enclosure = array();
516 $enclosure['url'] = $enclosure_o->url;
517 $enclosure['mimetype'] = $enclosure_o->mimetype;
518 $enclosure['size'] = $enclosure_o->size;
519 $enclosures[] = $enclosure;
523 if (!empty($enclosures)) {
524 $entry['enclosures'] = $enclosures;
528 $tag = new Notice_tag();
529 $tag->notice_id = $notice->id;
531 $entry['tags']=array();
532 while ($tag->fetch()) {
533 $entry['tags'][]=$tag->tag;
539 $entry['description'] = $entry['content'];
540 $entry['pubDate'] = common_date_rfc2822($notice->created);
541 $entry['guid'] = $entry['link'];
543 if (isset($notice->lat) && isset($notice->lon)) {
544 // This is the format that GeoJSON expects stuff to be in.
545 // showGeoRSS() below uses it for XML output, so we reuse it
546 $entry['geo'] = array('type' => 'Point',
547 'coordinates' => array((float) $notice->lat,
548 (float) $notice->lon));
550 $entry['geo'] = null;
553 Event::handle('EndRssEntryArray', array($notice, &$entry));
559 function twitterRelationshipArray($source, $target)
561 $relationship = array();
563 $relationship['source'] =
564 $this->relationshipDetailsArray($source, $target);
565 $relationship['target'] =
566 $this->relationshipDetailsArray($target, $source);
568 return array('relationship' => $relationship);
571 function relationshipDetailsArray($source, $target)
575 $details['screen_name'] = $source->nickname;
576 $details['followed_by'] = $target->isSubscribed($source);
577 $details['following'] = $source->isSubscribed($target);
579 $notifications = false;
581 if ($source->isSubscribed($target)) {
582 $sub = Subscription::pkeyGet(array('subscriber' =>
583 $source->id, 'subscribed' => $target->id));
586 $notifications = ($sub->jabber || $sub->sms);
590 $details['notifications_enabled'] = $notifications;
591 $details['blocking'] = $source->hasBlocked($target);
592 $details['id'] = intval($source->id);
597 function showTwitterXmlRelationship($relationship)
599 $this->elementStart('relationship');
601 foreach($relationship as $element => $value) {
602 if ($element == 'source' || $element == 'target') {
603 $this->elementStart($element);
604 $this->showXmlRelationshipDetails($value);
605 $this->elementEnd($element);
609 $this->elementEnd('relationship');
612 function showXmlRelationshipDetails($details)
614 foreach($details as $element => $value) {
615 $this->element($element, null, $value);
619 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
623 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
625 $this->elementStart($tag, $attrs);
626 foreach($twitter_status as $element => $value) {
629 $this->showTwitterXmlUser($twitter_status['user']);
632 $this->element($element, null, common_xml_safe_str($value));
635 $this->showXmlAttachments($twitter_status['attachments']);
638 $this->showGeoXML($value);
640 case 'retweeted_status':
641 $this->showTwitterXmlStatus($value, 'retweeted_status');
644 if (strncmp($element, 'statusnet_', 10) == 0) {
645 $this->element('statusnet:'.substr($element, 10), null, $value);
647 $this->element($element, null, $value);
651 $this->elementEnd($tag);
654 function showTwitterXmlGroup($twitter_group)
656 $this->elementStart('group');
657 foreach($twitter_group as $element => $value) {
658 $this->element($element, null, $value);
660 $this->elementEnd('group');
663 function showTwitterXmlList($twitter_list)
665 $this->elementStart('list');
666 foreach($twitter_list as $element => $value) {
667 if($element == 'user') {
668 $this->showTwitterXmlUser($value, 'user');
671 $this->element($element, null, $value);
674 $this->elementEnd('list');
677 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
681 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
683 $this->elementStart($role, $attrs);
684 foreach($twitter_user as $element => $value) {
685 if ($element == 'status') {
686 $this->showTwitterXmlStatus($twitter_user['status']);
687 } else if (strncmp($element, 'statusnet_', 10) == 0) {
688 $this->element('statusnet:'.substr($element, 10), null, $value);
690 $this->element($element, null, $value);
693 $this->elementEnd($role);
696 function showXmlAttachments($attachments) {
697 if (!empty($attachments)) {
698 $this->elementStart('attachments', array('type' => 'array'));
699 foreach ($attachments as $attachment) {
701 $attrs['url'] = $attachment['url'];
702 $attrs['mimetype'] = $attachment['mimetype'];
703 $attrs['size'] = $attachment['size'];
704 $this->element('enclosure', $attrs, '');
706 $this->elementEnd('attachments');
710 function showGeoXML($geo)
714 $this->element('geo');
716 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
717 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
718 $this->elementEnd('geo');
722 function showGeoRSS($geo)
728 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
733 function showTwitterRssItem($entry)
735 $this->elementStart('item');
736 $this->element('title', null, $entry['title']);
737 $this->element('description', null, $entry['description']);
738 $this->element('pubDate', null, $entry['pubDate']);
739 $this->element('guid', null, $entry['guid']);
740 $this->element('link', null, $entry['link']);
742 // RSS only supports 1 enclosure per item
743 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
744 $enclosure = $entry['enclosures'][0];
745 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
748 if(array_key_exists('tags', $entry)){
749 foreach($entry['tags'] as $tag){
750 $this->element('category', null,$tag);
754 $this->showGeoRSS($entry['geo']);
755 $this->elementEnd('item');
758 function showJsonObjects($objects)
760 print(json_encode($objects));
763 function showSingleXmlStatus($notice)
765 $this->initDocument('xml');
766 $twitter_status = $this->twitterStatusArray($notice);
767 $this->showTwitterXmlStatus($twitter_status, 'status', true);
768 $this->endDocument('xml');
771 function showSingleAtomStatus($notice)
773 header('Content-Type: application/atom+xml; charset=utf-8');
774 print $notice->asAtomEntry(true, true, true, $this->auth_user);
777 function show_single_json_status($notice)
779 $this->initDocument('json');
780 $status = $this->twitterStatusArray($notice);
781 $this->showJsonObjects($status);
782 $this->endDocument('json');
785 function showXmlTimeline($notice)
787 $this->initDocument('xml');
788 $this->elementStart('statuses', array('type' => 'array',
789 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
791 if (is_array($notice)) {
792 $notice = new ArrayWrapper($notice);
795 while ($notice->fetch()) {
797 $twitter_status = $this->twitterStatusArray($notice);
798 $this->showTwitterXmlStatus($twitter_status);
799 } catch (Exception $e) {
800 common_log(LOG_ERR, $e->getMessage());
805 $this->elementEnd('statuses');
806 $this->endDocument('xml');
809 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
811 $this->initDocument('rss');
813 $this->element('title', null, $title);
814 $this->element('link', null, $link);
816 if (!is_null($self)) {
820 'type' => 'application/rss+xml',
827 if (!is_null($suplink)) {
828 // For FriendFeed's SUP protocol
829 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
830 'rel' => 'http://api.friendfeed.com/2008/03#sup',
832 'type' => 'application/json'));
835 if (!is_null($logo)) {
836 $this->elementStart('image');
837 $this->element('link', null, $link);
838 $this->element('title', null, $title);
839 $this->element('url', null, $logo);
840 $this->elementEnd('image');
843 $this->element('description', null, $subtitle);
844 $this->element('language', null, 'en-us');
845 $this->element('ttl', null, '40');
847 if (is_array($notice)) {
848 $notice = new ArrayWrapper($notice);
851 while ($notice->fetch()) {
853 $entry = $this->twitterRssEntryArray($notice);
854 $this->showTwitterRssItem($entry);
855 } catch (Exception $e) {
856 common_log(LOG_ERR, $e->getMessage());
857 // continue on exceptions
861 $this->endTwitterRss();
864 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
866 $this->initDocument('atom');
868 $this->element('title', null, $title);
869 $this->element('id', null, $id);
870 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
872 if (!is_null($logo)) {
873 $this->element('logo',null,$logo);
876 if (!is_null($suplink)) {
877 // For FriendFeed's SUP protocol
878 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
880 'type' => 'application/json'));
883 if (!is_null($selfuri)) {
884 $this->element('link', array('href' => $selfuri,
885 'rel' => 'self', 'type' => 'application/atom+xml'), null);
888 $this->element('updated', null, common_date_iso8601('now'));
889 $this->element('subtitle', null, $subtitle);
891 if (is_array($notice)) {
892 $notice = new ArrayWrapper($notice);
895 while ($notice->fetch()) {
897 $this->raw($notice->asAtomEntry());
898 } catch (Exception $e) {
899 common_log(LOG_ERR, $e->getMessage());
904 $this->endDocument('atom');
907 function showRssGroups($group, $title, $link, $subtitle)
909 $this->initDocument('rss');
911 $this->element('title', null, $title);
912 $this->element('link', null, $link);
913 $this->element('description', null, $subtitle);
914 $this->element('language', null, 'en-us');
915 $this->element('ttl', null, '40');
917 if (is_array($group)) {
918 foreach ($group as $g) {
919 $twitter_group = $this->twitterRssGroupArray($g);
920 $this->showTwitterRssItem($twitter_group);
923 while ($group->fetch()) {
924 $twitter_group = $this->twitterRssGroupArray($group);
925 $this->showTwitterRssItem($twitter_group);
929 $this->endTwitterRss();
932 function showTwitterAtomEntry($entry)
934 $this->elementStart('entry');
935 $this->element('title', null, common_xml_safe_str($entry['title']));
938 array('type' => 'html'),
939 common_xml_safe_str($entry['content'])
941 $this->element('id', null, $entry['id']);
942 $this->element('published', null, $entry['published']);
943 $this->element('updated', null, $entry['updated']);
944 $this->element('link', array('type' => 'text/html',
945 'href' => $entry['link'],
946 'rel' => 'alternate'));
947 $this->element('link', array('type' => $entry['avatar-type'],
948 'href' => $entry['avatar'],
950 $this->elementStart('author');
952 $this->element('name', null, $entry['author-name']);
953 $this->element('uri', null, $entry['author-uri']);
955 $this->elementEnd('author');
956 $this->elementEnd('entry');
959 function showXmlDirectMessage($dm, $namespaces=false)
963 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
965 $this->elementStart('direct_message', $attrs);
966 foreach($dm as $element => $value) {
970 $this->showTwitterXmlUser($value, $element);
973 $this->element($element, null, common_xml_safe_str($value));
976 $this->element($element, null, $value);
980 $this->elementEnd('direct_message');
983 function directMessageArray($message)
987 $from_profile = $message->getFrom();
988 $to_profile = $message->getTo();
990 $dmsg['id'] = intval($message->id);
991 $dmsg['sender_id'] = intval($from_profile);
992 $dmsg['text'] = trim($message->content);
993 $dmsg['recipient_id'] = intval($to_profile);
994 $dmsg['created_at'] = $this->dateTwitter($message->created);
995 $dmsg['sender_screen_name'] = $from_profile->nickname;
996 $dmsg['recipient_screen_name'] = $to_profile->nickname;
997 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
998 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1003 function rssDirectMessageArray($message)
1007 $from = $message->getFrom();
1009 $entry['title'] = sprintf('Message from %1$s to %2$s',
1010 $from->nickname, $message->getTo()->nickname);
1012 $entry['content'] = common_xml_safe_str($message->rendered);
1013 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1014 $entry['published'] = common_date_iso8601($message->created);
1016 $taguribase = TagURI::base();
1018 $entry['id'] = "tag:$taguribase:$entry[link]";
1019 $entry['updated'] = $entry['published'];
1021 $entry['author-name'] = $from->getBestName();
1022 $entry['author-uri'] = $from->homepage;
1024 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1026 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
1027 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
1029 // RSS item specific
1031 $entry['description'] = $entry['content'];
1032 $entry['pubDate'] = common_date_rfc2822($message->created);
1033 $entry['guid'] = $entry['link'];
1038 function showSingleXmlDirectMessage($message)
1040 $this->initDocument('xml');
1041 $dmsg = $this->directMessageArray($message);
1042 $this->showXmlDirectMessage($dmsg, true);
1043 $this->endDocument('xml');
1046 function showSingleJsonDirectMessage($message)
1048 $this->initDocument('json');
1049 $dmsg = $this->directMessageArray($message);
1050 $this->showJsonObjects($dmsg);
1051 $this->endDocument('json');
1054 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1056 $this->initDocument('atom');
1058 $this->element('title', null, common_xml_safe_str($title));
1059 $this->element('id', null, $id);
1060 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1062 if (!is_null($selfuri)) {
1063 $this->element('link', array('href' => $selfuri,
1064 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1067 $this->element('updated', null, common_date_iso8601('now'));
1068 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1070 if (is_array($group)) {
1071 foreach ($group as $g) {
1072 $this->raw($g->asAtomEntry());
1075 while ($group->fetch()) {
1076 $this->raw($group->asAtomEntry());
1080 $this->endDocument('atom');
1084 function showJsonTimeline($notice)
1086 $this->initDocument('json');
1088 $statuses = array();
1090 if (is_array($notice)) {
1091 $notice = new ArrayWrapper($notice);
1094 while ($notice->fetch()) {
1096 $twitter_status = $this->twitterStatusArray($notice);
1097 array_push($statuses, $twitter_status);
1098 } catch (Exception $e) {
1099 common_log(LOG_ERR, $e->getMessage());
1104 $this->showJsonObjects($statuses);
1106 $this->endDocument('json');
1109 function showJsonGroups($group)
1111 $this->initDocument('json');
1115 if (is_array($group)) {
1116 foreach ($group as $g) {
1117 $twitter_group = $this->twitterGroupArray($g);
1118 array_push($groups, $twitter_group);
1121 while ($group->fetch()) {
1122 $twitter_group = $this->twitterGroupArray($group);
1123 array_push($groups, $twitter_group);
1127 $this->showJsonObjects($groups);
1129 $this->endDocument('json');
1132 function showXmlGroups($group)
1135 $this->initDocument('xml');
1136 $this->elementStart('groups', array('type' => 'array'));
1138 if (is_array($group)) {
1139 foreach ($group as $g) {
1140 $twitter_group = $this->twitterGroupArray($g);
1141 $this->showTwitterXmlGroup($twitter_group);
1144 while ($group->fetch()) {
1145 $twitter_group = $this->twitterGroupArray($group);
1146 $this->showTwitterXmlGroup($twitter_group);
1150 $this->elementEnd('groups');
1151 $this->endDocument('xml');
1154 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1157 $this->initDocument('xml');
1158 $this->elementStart('lists_list');
1159 $this->elementStart('lists', array('type' => 'array'));
1161 if (is_array($list)) {
1162 foreach ($list as $l) {
1163 $twitter_list = $this->twitterListArray($l);
1164 $this->showTwitterXmlList($twitter_list);
1167 while ($list->fetch()) {
1168 $twitter_list = $this->twitterListArray($list);
1169 $this->showTwitterXmlList($twitter_list);
1173 $this->elementEnd('lists');
1175 $this->element('next_cursor', null, $next_cursor);
1176 $this->element('previous_cursor', null, $prev_cursor);
1178 $this->elementEnd('lists_list');
1179 $this->endDocument('xml');
1182 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1184 $this->initDocument('json');
1188 if (is_array($list)) {
1189 foreach ($list as $l) {
1190 $twitter_list = $this->twitterListArray($l);
1191 array_push($lists, $twitter_list);
1194 while ($list->fetch()) {
1195 $twitter_list = $this->twitterListArray($list);
1196 array_push($lists, $twitter_list);
1200 $lists_list = array(
1202 'next_cursor' => $next_cursor,
1203 'next_cursor_str' => strval($next_cursor),
1204 'previous_cursor' => $prev_cursor,
1205 'previous_cursor_str' => strval($prev_cursor)
1208 $this->showJsonObjects($lists_list);
1210 $this->endDocument('json');
1213 function showTwitterXmlUsers($user)
1215 $this->initDocument('xml');
1216 $this->elementStart('users', array('type' => 'array',
1217 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1219 if (is_array($user)) {
1220 foreach ($user as $u) {
1221 $twitter_user = $this->twitterUserArray($u);
1222 $this->showTwitterXmlUser($twitter_user);
1225 while ($user->fetch()) {
1226 $twitter_user = $this->twitterUserArray($user);
1227 $this->showTwitterXmlUser($twitter_user);
1231 $this->elementEnd('users');
1232 $this->endDocument('xml');
1235 function showJsonUsers($user)
1237 $this->initDocument('json');
1241 if (is_array($user)) {
1242 foreach ($user as $u) {
1243 $twitter_user = $this->twitterUserArray($u);
1244 array_push($users, $twitter_user);
1247 while ($user->fetch()) {
1248 $twitter_user = $this->twitterUserArray($user);
1249 array_push($users, $twitter_user);
1253 $this->showJsonObjects($users);
1255 $this->endDocument('json');
1258 function showSingleJsonGroup($group)
1260 $this->initDocument('json');
1261 $twitter_group = $this->twitterGroupArray($group);
1262 $this->showJsonObjects($twitter_group);
1263 $this->endDocument('json');
1266 function showSingleXmlGroup($group)
1268 $this->initDocument('xml');
1269 $twitter_group = $this->twitterGroupArray($group);
1270 $this->showTwitterXmlGroup($twitter_group);
1271 $this->endDocument('xml');
1274 function showSingleJsonList($list)
1276 $this->initDocument('json');
1277 $twitter_list = $this->twitterListArray($list);
1278 $this->showJsonObjects($twitter_list);
1279 $this->endDocument('json');
1282 function showSingleXmlList($list)
1284 $this->initDocument('xml');
1285 $twitter_list = $this->twitterListArray($list);
1286 $this->showTwitterXmlList($twitter_list);
1287 $this->endDocument('xml');
1290 function dateTwitter($dt)
1292 $dateStr = date('d F Y H:i:s', strtotime($dt));
1293 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1294 $d->setTimezone(new DateTimeZone(common_timezone()));
1295 return $d->format('D M d H:i:s O Y');
1298 function initDocument($type='xml')
1302 header('Content-Type: application/xml; charset=utf-8');
1306 header('Content-Type: application/json; charset=utf-8');
1308 // Check for JSONP callback
1309 if (isset($this->callback)) {
1310 print $this->callback . '(';
1314 header("Content-Type: application/rss+xml; charset=utf-8");
1315 $this->initTwitterRss();
1318 header('Content-Type: application/atom+xml; charset=utf-8');
1319 $this->initTwitterAtom();
1322 // TRANS: Client error on an API request with an unsupported data format.
1323 $this->clientError(_('Not a supported data format.'));
1330 function endDocument($type='xml')
1337 // Check for JSONP callback
1338 if (isset($this->callback)) {
1343 $this->endTwitterRss();
1346 $this->endTwitterRss();
1349 // TRANS: Client error on an API request with an unsupported data format.
1350 $this->clientError(_('Not a supported data format.'));
1356 function clientError($msg, $code = 400, $format = null)
1358 $action = $this->trimmed('action');
1359 if ($format === null) {
1360 $format = $this->format;
1363 common_debug("User error '$code' on '$action': $msg", __FILE__);
1365 if (!array_key_exists($code, ClientErrorAction::$status)) {
1369 $status_string = ClientErrorAction::$status[$code];
1371 // Do not emit error header for JSONP
1372 if (!isset($this->callback)) {
1373 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1378 $this->initDocument('xml');
1379 $this->elementStart('hash');
1380 $this->element('error', null, $msg);
1381 $this->element('request', null, $_SERVER['REQUEST_URI']);
1382 $this->elementEnd('hash');
1383 $this->endDocument('xml');
1386 $this->initDocument('json');
1387 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1388 print(json_encode($error_array));
1389 $this->endDocument('json');
1392 header('Content-Type: text/plain; charset=utf-8');
1396 // If user didn't request a useful format, throw a regular client error
1397 throw new ClientException($msg, $code);
1401 function serverError($msg, $code = 500, $content_type = null)
1403 $action = $this->trimmed('action');
1404 if ($content_type === null) {
1405 $content_type = $this->format;
1408 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1410 if (!array_key_exists($code, ServerErrorAction::$status)) {
1414 $status_string = ServerErrorAction::$status[$code];
1416 // Do not emit error header for JSONP
1417 if (!isset($this->callback)) {
1418 header('HTTP/1.1 '.$code.' '.$status_string);
1421 if ($content_type == 'xml') {
1422 $this->initDocument('xml');
1423 $this->elementStart('hash');
1424 $this->element('error', null, $msg);
1425 $this->element('request', null, $_SERVER['REQUEST_URI']);
1426 $this->elementEnd('hash');
1427 $this->endDocument('xml');
1429 $this->initDocument('json');
1430 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1431 print(json_encode($error_array));
1432 $this->endDocument('json');
1436 function initTwitterRss()
1439 $this->elementStart(
1443 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1444 'xmlns:georss' => 'http://www.georss.org/georss'
1447 $this->elementStart('channel');
1448 Event::handle('StartApiRss', array($this));
1451 function endTwitterRss()
1453 $this->elementEnd('channel');
1454 $this->elementEnd('rss');
1458 function initTwitterAtom()
1461 // FIXME: don't hardcode the language here!
1462 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1463 'xml:lang' => 'en-US',
1464 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1467 function endTwitterAtom()
1469 $this->elementEnd('feed');
1473 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1475 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1476 switch ($content_type) {
1478 $this->showTwitterXmlUser($profile_array);
1481 $this->showJsonObjects($profile_array);
1484 // TRANS: Client error on an API request with an unsupported data format.
1485 $this->clientError(_('Not a supported data format.'));
1491 private static function is_decimal($str)
1493 return preg_match('/^[0-9]+$/', $str);
1496 function getTargetUser($id)
1499 // Twitter supports these other ways of passing the user ID
1500 if (self::is_decimal($this->arg('id'))) {
1501 return User::staticGet($this->arg('id'));
1502 } else if ($this->arg('id')) {
1503 $nickname = common_canonical_nickname($this->arg('id'));
1504 return User::staticGet('nickname', $nickname);
1505 } else if ($this->arg('user_id')) {
1506 // This is to ensure that a non-numeric user_id still
1507 // overrides screen_name even if it doesn't get used
1508 if (self::is_decimal($this->arg('user_id'))) {
1509 return User::staticGet('id', $this->arg('user_id'));
1511 } else if ($this->arg('screen_name')) {
1512 $nickname = common_canonical_nickname($this->arg('screen_name'));
1513 return User::staticGet('nickname', $nickname);
1515 // Fall back to trying the currently authenticated user
1516 return $this->auth_user;
1519 } else if (self::is_decimal($id)) {
1520 return User::staticGet($id);
1522 $nickname = common_canonical_nickname($id);
1523 return User::staticGet('nickname', $nickname);
1527 function getTargetProfile($id)
1531 // Twitter supports these other ways of passing the user ID
1532 if (self::is_decimal($this->arg('id'))) {
1533 return Profile::staticGet($this->arg('id'));
1534 } else if ($this->arg('id')) {
1535 // Screen names currently can only uniquely identify a local user.
1536 $nickname = common_canonical_nickname($this->arg('id'));
1537 $user = User::staticGet('nickname', $nickname);
1538 return $user ? $user->getProfile() : null;
1539 } else if ($this->arg('user_id')) {
1540 // This is to ensure that a non-numeric user_id still
1541 // overrides screen_name even if it doesn't get used
1542 if (self::is_decimal($this->arg('user_id'))) {
1543 return Profile::staticGet('id', $this->arg('user_id'));
1545 } else if ($this->arg('screen_name')) {
1546 $nickname = common_canonical_nickname($this->arg('screen_name'));
1547 $user = User::staticGet('nickname', $nickname);
1548 return $user ? $user->getProfile() : null;
1550 } else if (self::is_decimal($id)) {
1551 return Profile::staticGet($id);
1553 $nickname = common_canonical_nickname($id);
1554 $user = User::staticGet('nickname', $nickname);
1555 return $user ? $user->getProfile() : null;
1559 function getTargetGroup($id)
1562 if (self::is_decimal($this->arg('id'))) {
1563 return User_group::staticGet('id', $this->arg('id'));
1564 } else if ($this->arg('id')) {
1565 return User_group::getForNickname($this->arg('id'));
1566 } else if ($this->arg('group_id')) {
1567 // This is to ensure that a non-numeric group_id still
1568 // overrides group_name even if it doesn't get used
1569 if (self::is_decimal($this->arg('group_id'))) {
1570 return User_group::staticGet('id', $this->arg('group_id'));
1572 } else if ($this->arg('group_name')) {
1573 return User_group::getForNickname($this->arg('group_name'));
1576 } else if (self::is_decimal($id)) {
1577 return User_group::staticGet('id', $id);
1579 return User_group::getForNickname($id);
1583 function getTargetList($user=null, $id=null)
1585 $tagger = $this->getTargetUser($user);
1589 $id = $this->arg('id');
1593 if (is_numeric($id)) {
1594 $list = Profile_list::staticGet('id', $id);
1596 // only if the list with the id belongs to the tagger
1597 if(empty($list) || $list->tagger != $tagger->id) {
1602 $tag = common_canonical_tag($id);
1603 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1606 if (!empty($list) && $list->private) {
1607 if ($this->auth_user->id == $list->tagger) {
1618 * Returns query argument or default value if not found. Certain
1619 * parameters used throughout the API are lightly scrubbed and
1620 * bounds checked. This overrides Action::arg().
1622 * @param string $key requested argument
1623 * @param string $def default value to return if $key is not provided
1627 function arg($key, $def=null)
1629 // XXX: Do even more input validation/scrubbing?
1631 if (array_key_exists($key, $this->args)) {
1634 $page = (int)$this->args['page'];
1635 return ($page < 1) ? 1 : $page;
1637 $count = (int)$this->args['count'];
1640 } elseif ($count > 200) {
1646 $since_id = (int)$this->args['since_id'];
1647 return ($since_id < 1) ? 0 : $since_id;
1649 $max_id = (int)$this->args['max_id'];
1650 return ($max_id < 1) ? 0 : $max_id;
1652 return parent::arg($key, $def);
1660 * Calculate the complete URI that called up this action. Used for
1661 * Atom rel="self" links. Warning: this is funky.
1663 * @return string URL a URL suitable for rel="self" Atom links
1665 function getSelfUri()
1667 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1669 $id = $this->arg('id');
1670 $aargs = array('format' => $this->format);
1675 $tag = $this->arg('tag');
1677 $aargs['tag'] = $tag;
1680 parse_str($_SERVER['QUERY_STRING'], $params);
1682 if (!empty($params)) {
1683 unset($params['p']);
1684 $pstring = http_build_query($params);
1687 $uri = common_local_url($action, $aargs);
1689 if (!empty($pstring)) {
1690 $uri .= '?' . $pstring;