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();
221 // Note: some profiles don't have an associated user
223 $twitter_user['friends_count'] = $profile->subscriptionCount();
225 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
227 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
231 if (!empty($user) && $user->timezone) {
232 $timezone = $user->timezone;
236 $t->setTimezone(new DateTimeZone($timezone));
238 $twitter_user['utc_offset'] = $t->format('Z');
239 $twitter_user['time_zone'] = $timezone;
240 $twitter_user['statuses_count'] = $profile->noticeCount();
242 // Is the requesting user following this user?
243 $twitter_user['following'] = false;
244 $twitter_user['statusnet:blocking'] = false;
245 $twitter_user['notifications'] = false;
247 if (isset($this->auth_user)) {
249 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
250 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
253 $sub = Subscription::pkeyGet(array('subscriber' =>
254 $this->auth_user->id,
255 'subscribed' => $profile->id));
258 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
263 $notice = $profile->getCurrentNotice();
266 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
270 // StatusNet-specific
272 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
274 return $twitter_user;
277 function twitterStatusArray($notice, $include_user=true)
279 $base = $this->twitterSimpleStatusArray($notice, $include_user);
281 if (!empty($notice->repeat_of)) {
282 $original = Notice::staticGet('id', $notice->repeat_of);
283 if (!empty($original)) {
284 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
285 $base['retweeted_status'] = $original_array;
292 function twitterSimpleStatusArray($notice, $include_user=true)
294 $profile = $notice->getProfile();
296 $twitter_status = array();
297 $twitter_status['text'] = $notice->content;
298 $twitter_status['truncated'] = false; # Not possible on StatusNet
299 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
300 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
301 intval($notice->reply_to) : null;
305 $ns = $notice->getSource();
307 if (!empty($ns->name) && !empty($ns->url)) {
308 $source = '<a href="'
309 . htmlspecialchars($ns->url)
310 . '" rel="nofollow">'
311 . htmlspecialchars($ns->name)
318 $twitter_status['source'] = $source;
319 $twitter_status['id'] = intval($notice->id);
321 $replier_profile = null;
323 if ($notice->reply_to) {
324 $reply = Notice::staticGet(intval($notice->reply_to));
326 $replier_profile = $reply->getProfile();
330 $twitter_status['in_reply_to_user_id'] =
331 ($replier_profile) ? intval($replier_profile->id) : null;
332 $twitter_status['in_reply_to_screen_name'] =
333 ($replier_profile) ? $replier_profile->nickname : null;
335 if (isset($notice->lat) && isset($notice->lon)) {
336 // This is the format that GeoJSON expects stuff to be in
337 $twitter_status['geo'] = array('type' => 'Point',
338 'coordinates' => array((float) $notice->lat,
339 (float) $notice->lon));
341 $twitter_status['geo'] = null;
344 if (isset($this->auth_user)) {
345 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
347 $twitter_status['favorited'] = false;
351 $attachments = $notice->attachments();
353 if (!empty($attachments)) {
355 $twitter_status['attachments'] = array();
357 foreach ($attachments as $attachment) {
358 $enclosure_o=$attachment->getEnclosure();
360 $enclosure = array();
361 $enclosure['url'] = $enclosure_o->url;
362 $enclosure['mimetype'] = $enclosure_o->mimetype;
363 $enclosure['size'] = $enclosure_o->size;
364 $twitter_status['attachments'][] = $enclosure;
369 if ($include_user && $profile) {
370 // Don't get notice (recursive!)
371 $twitter_user = $this->twitterUserArray($profile, false);
372 $twitter_status['user'] = $twitter_user;
375 // StatusNet-specific
377 $twitter_status['statusnet_html'] = $notice->rendered;
379 return $twitter_status;
382 function twitterGroupArray($group)
384 $twitter_group = array();
386 $twitter_group['id'] = intval($group->id);
387 $twitter_group['url'] = $group->permalink();
388 $twitter_group['nickname'] = $group->nickname;
389 $twitter_group['fullname'] = $group->fullname;
391 if (isset($this->auth_user)) {
392 $twitter_group['member'] = $this->auth_user->isMember($group);
393 $twitter_group['blocked'] = Group_block::isBlocked(
395 $this->auth_user->getProfile()
399 $twitter_group['member_count'] = $group->getMemberCount();
400 $twitter_group['original_logo'] = $group->original_logo;
401 $twitter_group['homepage_logo'] = $group->homepage_logo;
402 $twitter_group['stream_logo'] = $group->stream_logo;
403 $twitter_group['mini_logo'] = $group->mini_logo;
404 $twitter_group['homepage'] = $group->homepage;
405 $twitter_group['description'] = $group->description;
406 $twitter_group['location'] = $group->location;
407 $twitter_group['created'] = $this->dateTwitter($group->created);
408 $twitter_group['modified'] = $this->dateTwitter($group->modified);
410 return $twitter_group;
413 function twitterRssGroupArray($group)
416 $entry['content']=$group->description;
417 $entry['title']=$group->nickname;
418 $entry['link']=$group->permalink();
419 $entry['published']=common_date_iso8601($group->created);
420 $entry['updated']==common_date_iso8601($group->modified);
421 $taguribase = common_config('integration', 'groupuri');
422 $entry['id'] = "group:$groupuribase:$entry[link]";
424 $entry['description'] = $entry['content'];
425 $entry['pubDate'] = common_date_rfc2822($group->created);
426 $entry['guid'] = $entry['link'];
431 function twitterListArray($list)
433 $profile = Profile::staticGet('id', $list->tagger);
435 $twitter_list = array();
436 $twitter_list['id'] = $list->id;
437 $twitter_list['name'] = $list->tag;
438 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
439 $twitter_list['slug'] = $list->tag;
440 $twitter_list['description'] = $list->description;
441 $twitter_list['subscriber_count'] = $list->subscriberCount();
442 $twitter_list['member_count'] = $list->taggedCount();
443 $twitter_list['uri'] = $list->getUri();
445 if (isset($this->auth_user)) {
446 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
448 $twitter_list['following'] = false;
451 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
452 $twitter_list['user'] = $this->twitterUserArray($profile, false);
454 return $twitter_list;
457 function twitterRssEntryArray($notice)
461 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
462 $profile = $notice->getProfile();
464 // We trim() to avoid extraneous whitespace in the output
466 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
467 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
468 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
469 $entry['published'] = common_date_iso8601($notice->created);
471 $taguribase = TagURI::base();
472 $entry['id'] = "tag:$taguribase:$entry[link]";
474 $entry['updated'] = $entry['published'];
475 $entry['author'] = $profile->getBestName();
478 $attachments = $notice->attachments();
479 $enclosures = array();
481 foreach ($attachments as $attachment) {
482 $enclosure_o=$attachment->getEnclosure();
484 $enclosure = array();
485 $enclosure['url'] = $enclosure_o->url;
486 $enclosure['mimetype'] = $enclosure_o->mimetype;
487 $enclosure['size'] = $enclosure_o->size;
488 $enclosures[] = $enclosure;
492 if (!empty($enclosures)) {
493 $entry['enclosures'] = $enclosures;
497 $tag = new Notice_tag();
498 $tag->notice_id = $notice->id;
500 $entry['tags']=array();
501 while ($tag->fetch()) {
502 $entry['tags'][]=$tag->tag;
508 $entry['description'] = $entry['content'];
509 $entry['pubDate'] = common_date_rfc2822($notice->created);
510 $entry['guid'] = $entry['link'];
512 if (isset($notice->lat) && isset($notice->lon)) {
513 // This is the format that GeoJSON expects stuff to be in.
514 // showGeoRSS() below uses it for XML output, so we reuse it
515 $entry['geo'] = array('type' => 'Point',
516 'coordinates' => array((float) $notice->lat,
517 (float) $notice->lon));
519 $entry['geo'] = null;
522 Event::handle('EndRssEntryArray', array($notice, &$entry));
528 function twitterRelationshipArray($source, $target)
530 $relationship = array();
532 $relationship['source'] =
533 $this->relationshipDetailsArray($source, $target);
534 $relationship['target'] =
535 $this->relationshipDetailsArray($target, $source);
537 return array('relationship' => $relationship);
540 function relationshipDetailsArray($source, $target)
544 $details['screen_name'] = $source->nickname;
545 $details['followed_by'] = $target->isSubscribed($source);
546 $details['following'] = $source->isSubscribed($target);
548 $notifications = false;
550 if ($source->isSubscribed($target)) {
551 $sub = Subscription::pkeyGet(array('subscriber' =>
552 $source->id, 'subscribed' => $target->id));
555 $notifications = ($sub->jabber || $sub->sms);
559 $details['notifications_enabled'] = $notifications;
560 $details['blocking'] = $source->hasBlocked($target);
561 $details['id'] = intval($source->id);
566 function showTwitterXmlRelationship($relationship)
568 $this->elementStart('relationship');
570 foreach($relationship as $element => $value) {
571 if ($element == 'source' || $element == 'target') {
572 $this->elementStart($element);
573 $this->showXmlRelationshipDetails($value);
574 $this->elementEnd($element);
578 $this->elementEnd('relationship');
581 function showXmlRelationshipDetails($details)
583 foreach($details as $element => $value) {
584 $this->element($element, null, $value);
588 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
592 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
594 $this->elementStart($tag, $attrs);
595 foreach($twitter_status as $element => $value) {
598 $this->showTwitterXmlUser($twitter_status['user']);
601 $this->element($element, null, common_xml_safe_str($value));
604 $this->showXmlAttachments($twitter_status['attachments']);
607 $this->showGeoXML($value);
609 case 'retweeted_status':
610 $this->showTwitterXmlStatus($value, 'retweeted_status');
613 if (strncmp($element, 'statusnet_', 10) == 0) {
614 $this->element('statusnet:'.substr($element, 10), null, $value);
616 $this->element($element, null, $value);
620 $this->elementEnd($tag);
623 function showTwitterXmlGroup($twitter_group)
625 $this->elementStart('group');
626 foreach($twitter_group as $element => $value) {
627 $this->element($element, null, $value);
629 $this->elementEnd('group');
632 function showTwitterXmlList($twitter_list)
634 $this->elementStart('list');
635 foreach($twitter_list as $element => $value) {
636 if($element == 'user') {
637 $this->showTwitterXmlUser($value, 'user');
640 $this->element($element, null, $value);
643 $this->elementEnd('list');
646 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
650 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
652 $this->elementStart($role, $attrs);
653 foreach($twitter_user as $element => $value) {
654 if ($element == 'status') {
655 $this->showTwitterXmlStatus($twitter_user['status']);
656 } else if (strncmp($element, 'statusnet_', 10) == 0) {
657 $this->element('statusnet:'.substr($element, 10), null, $value);
659 $this->element($element, null, $value);
662 $this->elementEnd($role);
665 function showXmlAttachments($attachments) {
666 if (!empty($attachments)) {
667 $this->elementStart('attachments', array('type' => 'array'));
668 foreach ($attachments as $attachment) {
670 $attrs['url'] = $attachment['url'];
671 $attrs['mimetype'] = $attachment['mimetype'];
672 $attrs['size'] = $attachment['size'];
673 $this->element('enclosure', $attrs, '');
675 $this->elementEnd('attachments');
679 function showGeoXML($geo)
683 $this->element('geo');
685 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
686 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
687 $this->elementEnd('geo');
691 function showGeoRSS($geo)
697 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
702 function showTwitterRssItem($entry)
704 $this->elementStart('item');
705 $this->element('title', null, $entry['title']);
706 $this->element('description', null, $entry['description']);
707 $this->element('pubDate', null, $entry['pubDate']);
708 $this->element('guid', null, $entry['guid']);
709 $this->element('link', null, $entry['link']);
711 // RSS only supports 1 enclosure per item
712 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
713 $enclosure = $entry['enclosures'][0];
714 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
717 if(array_key_exists('tags', $entry)){
718 foreach($entry['tags'] as $tag){
719 $this->element('category', null,$tag);
723 $this->showGeoRSS($entry['geo']);
724 $this->elementEnd('item');
727 function showJsonObjects($objects)
729 print(json_encode($objects));
732 function showSingleXmlStatus($notice)
734 $this->initDocument('xml');
735 $twitter_status = $this->twitterStatusArray($notice);
736 $this->showTwitterXmlStatus($twitter_status, 'status', true);
737 $this->endDocument('xml');
740 function showSingleAtomStatus($notice)
742 header('Content-Type: application/atom+xml; charset=utf-8');
743 print $notice->asAtomEntry(true, true, true, $this->auth_user);
746 function show_single_json_status($notice)
748 $this->initDocument('json');
749 $status = $this->twitterStatusArray($notice);
750 $this->showJsonObjects($status);
751 $this->endDocument('json');
754 function showXmlTimeline($notice)
756 $this->initDocument('xml');
757 $this->elementStart('statuses', array('type' => 'array',
758 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
760 if (is_array($notice)) {
761 $notice = new ArrayWrapper($notice);
764 while ($notice->fetch()) {
766 $twitter_status = $this->twitterStatusArray($notice);
767 $this->showTwitterXmlStatus($twitter_status);
768 } catch (Exception $e) {
769 common_log(LOG_ERR, $e->getMessage());
774 $this->elementEnd('statuses');
775 $this->endDocument('xml');
778 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
780 $this->initDocument('rss');
782 $this->element('title', null, $title);
783 $this->element('link', null, $link);
785 if (!is_null($self)) {
789 'type' => 'application/rss+xml',
796 if (!is_null($suplink)) {
797 // For FriendFeed's SUP protocol
798 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
799 'rel' => 'http://api.friendfeed.com/2008/03#sup',
801 'type' => 'application/json'));
804 if (!is_null($logo)) {
805 $this->elementStart('image');
806 $this->element('link', null, $link);
807 $this->element('title', null, $title);
808 $this->element('url', null, $logo);
809 $this->elementEnd('image');
812 $this->element('description', null, $subtitle);
813 $this->element('language', null, 'en-us');
814 $this->element('ttl', null, '40');
816 if (is_array($notice)) {
817 $notice = new ArrayWrapper($notice);
820 while ($notice->fetch()) {
822 $entry = $this->twitterRssEntryArray($notice);
823 $this->showTwitterRssItem($entry);
824 } catch (Exception $e) {
825 common_log(LOG_ERR, $e->getMessage());
826 // continue on exceptions
830 $this->endTwitterRss();
833 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
835 $this->initDocument('atom');
837 $this->element('title', null, $title);
838 $this->element('id', null, $id);
839 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
841 if (!is_null($logo)) {
842 $this->element('logo',null,$logo);
845 if (!is_null($suplink)) {
846 // For FriendFeed's SUP protocol
847 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
849 'type' => 'application/json'));
852 if (!is_null($selfuri)) {
853 $this->element('link', array('href' => $selfuri,
854 'rel' => 'self', 'type' => 'application/atom+xml'), null);
857 $this->element('updated', null, common_date_iso8601('now'));
858 $this->element('subtitle', null, $subtitle);
860 if (is_array($notice)) {
861 $notice = new ArrayWrapper($notice);
864 while ($notice->fetch()) {
866 $this->raw($notice->asAtomEntry());
867 } catch (Exception $e) {
868 common_log(LOG_ERR, $e->getMessage());
873 $this->endDocument('atom');
876 function showRssGroups($group, $title, $link, $subtitle)
878 $this->initDocument('rss');
880 $this->element('title', null, $title);
881 $this->element('link', null, $link);
882 $this->element('description', null, $subtitle);
883 $this->element('language', null, 'en-us');
884 $this->element('ttl', null, '40');
886 if (is_array($group)) {
887 foreach ($group as $g) {
888 $twitter_group = $this->twitterRssGroupArray($g);
889 $this->showTwitterRssItem($twitter_group);
892 while ($group->fetch()) {
893 $twitter_group = $this->twitterRssGroupArray($group);
894 $this->showTwitterRssItem($twitter_group);
898 $this->endTwitterRss();
901 function showTwitterAtomEntry($entry)
903 $this->elementStart('entry');
904 $this->element('title', null, common_xml_safe_str($entry['title']));
907 array('type' => 'html'),
908 common_xml_safe_str($entry['content'])
910 $this->element('id', null, $entry['id']);
911 $this->element('published', null, $entry['published']);
912 $this->element('updated', null, $entry['updated']);
913 $this->element('link', array('type' => 'text/html',
914 'href' => $entry['link'],
915 'rel' => 'alternate'));
916 $this->element('link', array('type' => $entry['avatar-type'],
917 'href' => $entry['avatar'],
919 $this->elementStart('author');
921 $this->element('name', null, $entry['author-name']);
922 $this->element('uri', null, $entry['author-uri']);
924 $this->elementEnd('author');
925 $this->elementEnd('entry');
928 function showXmlDirectMessage($dm, $namespaces=false)
932 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
934 $this->elementStart('direct_message', $attrs);
935 foreach($dm as $element => $value) {
939 $this->showTwitterXmlUser($value, $element);
942 $this->element($element, null, common_xml_safe_str($value));
945 $this->element($element, null, $value);
949 $this->elementEnd('direct_message');
952 function directMessageArray($message)
956 $from_profile = $message->getFrom();
957 $to_profile = $message->getTo();
959 $dmsg['id'] = intval($message->id);
960 $dmsg['sender_id'] = intval($from_profile);
961 $dmsg['text'] = trim($message->content);
962 $dmsg['recipient_id'] = intval($to_profile);
963 $dmsg['created_at'] = $this->dateTwitter($message->created);
964 $dmsg['sender_screen_name'] = $from_profile->nickname;
965 $dmsg['recipient_screen_name'] = $to_profile->nickname;
966 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
967 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
972 function rssDirectMessageArray($message)
976 $from = $message->getFrom();
978 $entry['title'] = sprintf('Message from %1$s to %2$s',
979 $from->nickname, $message->getTo()->nickname);
981 $entry['content'] = common_xml_safe_str($message->rendered);
982 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
983 $entry['published'] = common_date_iso8601($message->created);
985 $taguribase = TagURI::base();
987 $entry['id'] = "tag:$taguribase:$entry[link]";
988 $entry['updated'] = $entry['published'];
990 $entry['author-name'] = $from->getBestName();
991 $entry['author-uri'] = $from->homepage;
993 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
995 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
996 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
1000 $entry['description'] = $entry['content'];
1001 $entry['pubDate'] = common_date_rfc2822($message->created);
1002 $entry['guid'] = $entry['link'];
1007 function showSingleXmlDirectMessage($message)
1009 $this->initDocument('xml');
1010 $dmsg = $this->directMessageArray($message);
1011 $this->showXmlDirectMessage($dmsg, true);
1012 $this->endDocument('xml');
1015 function showSingleJsonDirectMessage($message)
1017 $this->initDocument('json');
1018 $dmsg = $this->directMessageArray($message);
1019 $this->showJsonObjects($dmsg);
1020 $this->endDocument('json');
1023 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1025 $this->initDocument('atom');
1027 $this->element('title', null, common_xml_safe_str($title));
1028 $this->element('id', null, $id);
1029 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1031 if (!is_null($selfuri)) {
1032 $this->element('link', array('href' => $selfuri,
1033 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1036 $this->element('updated', null, common_date_iso8601('now'));
1037 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1039 if (is_array($group)) {
1040 foreach ($group as $g) {
1041 $this->raw($g->asAtomEntry());
1044 while ($group->fetch()) {
1045 $this->raw($group->asAtomEntry());
1049 $this->endDocument('atom');
1053 function showJsonTimeline($notice)
1055 $this->initDocument('json');
1057 $statuses = array();
1059 if (is_array($notice)) {
1060 $notice = new ArrayWrapper($notice);
1063 while ($notice->fetch()) {
1065 $twitter_status = $this->twitterStatusArray($notice);
1066 array_push($statuses, $twitter_status);
1067 } catch (Exception $e) {
1068 common_log(LOG_ERR, $e->getMessage());
1073 $this->showJsonObjects($statuses);
1075 $this->endDocument('json');
1078 function showJsonGroups($group)
1080 $this->initDocument('json');
1084 if (is_array($group)) {
1085 foreach ($group as $g) {
1086 $twitter_group = $this->twitterGroupArray($g);
1087 array_push($groups, $twitter_group);
1090 while ($group->fetch()) {
1091 $twitter_group = $this->twitterGroupArray($group);
1092 array_push($groups, $twitter_group);
1096 $this->showJsonObjects($groups);
1098 $this->endDocument('json');
1101 function showXmlGroups($group)
1104 $this->initDocument('xml');
1105 $this->elementStart('groups', array('type' => 'array'));
1107 if (is_array($group)) {
1108 foreach ($group as $g) {
1109 $twitter_group = $this->twitterGroupArray($g);
1110 $this->showTwitterXmlGroup($twitter_group);
1113 while ($group->fetch()) {
1114 $twitter_group = $this->twitterGroupArray($group);
1115 $this->showTwitterXmlGroup($twitter_group);
1119 $this->elementEnd('groups');
1120 $this->endDocument('xml');
1123 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1126 $this->initDocument('xml');
1127 $this->elementStart('lists_list');
1128 $this->elementStart('lists', array('type' => 'array'));
1130 if (is_array($list)) {
1131 foreach ($list as $l) {
1132 $twitter_list = $this->twitterListArray($l);
1133 $this->showTwitterXmlList($twitter_list);
1136 while ($list->fetch()) {
1137 $twitter_list = $this->twitterListArray($list);
1138 $this->showTwitterXmlList($twitter_list);
1142 $this->elementEnd('lists');
1144 $this->element('next_cursor', null, $next_cursor);
1145 $this->element('previous_cursor', null, $prev_cursor);
1147 $this->elementEnd('lists_list');
1148 $this->endDocument('xml');
1151 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1153 $this->initDocument('json');
1157 if (is_array($list)) {
1158 foreach ($list as $l) {
1159 $twitter_list = $this->twitterListArray($l);
1160 array_push($lists, $twitter_list);
1163 while ($list->fetch()) {
1164 $twitter_list = $this->twitterListArray($list);
1165 array_push($lists, $twitter_list);
1169 $lists_list = array(
1171 'next_cursor' => $next_cursor,
1172 'next_cursor_str' => strval($next_cursor),
1173 'previous_cursor' => $prev_cursor,
1174 'previous_cursor_str' => strval($prev_cursor)
1177 $this->showJsonObjects($lists_list);
1179 $this->endDocument('json');
1182 function showTwitterXmlUsers($user)
1184 $this->initDocument('xml');
1185 $this->elementStart('users', array('type' => 'array',
1186 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1188 if (is_array($user)) {
1189 foreach ($user as $u) {
1190 $twitter_user = $this->twitterUserArray($u);
1191 $this->showTwitterXmlUser($twitter_user);
1194 while ($user->fetch()) {
1195 $twitter_user = $this->twitterUserArray($user);
1196 $this->showTwitterXmlUser($twitter_user);
1200 $this->elementEnd('users');
1201 $this->endDocument('xml');
1204 function showJsonUsers($user)
1206 $this->initDocument('json');
1210 if (is_array($user)) {
1211 foreach ($user as $u) {
1212 $twitter_user = $this->twitterUserArray($u);
1213 array_push($users, $twitter_user);
1216 while ($user->fetch()) {
1217 $twitter_user = $this->twitterUserArray($user);
1218 array_push($users, $twitter_user);
1222 $this->showJsonObjects($users);
1224 $this->endDocument('json');
1227 function showSingleJsonGroup($group)
1229 $this->initDocument('json');
1230 $twitter_group = $this->twitterGroupArray($group);
1231 $this->showJsonObjects($twitter_group);
1232 $this->endDocument('json');
1235 function showSingleXmlGroup($group)
1237 $this->initDocument('xml');
1238 $twitter_group = $this->twitterGroupArray($group);
1239 $this->showTwitterXmlGroup($twitter_group);
1240 $this->endDocument('xml');
1243 function showSingleJsonList($list)
1245 $this->initDocument('json');
1246 $twitter_list = $this->twitterListArray($list);
1247 $this->showJsonObjects($twitter_list);
1248 $this->endDocument('json');
1251 function showSingleXmlList($list)
1253 $this->initDocument('xml');
1254 $twitter_list = $this->twitterListArray($list);
1255 $this->showTwitterXmlList($twitter_list);
1256 $this->endDocument('xml');
1259 function dateTwitter($dt)
1261 $dateStr = date('d F Y H:i:s', strtotime($dt));
1262 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1263 $d->setTimezone(new DateTimeZone(common_timezone()));
1264 return $d->format('D M d H:i:s O Y');
1267 function initDocument($type='xml')
1271 header('Content-Type: application/xml; charset=utf-8');
1275 header('Content-Type: application/json; charset=utf-8');
1277 // Check for JSONP callback
1278 if (isset($this->callback)) {
1279 print $this->callback . '(';
1283 header("Content-Type: application/rss+xml; charset=utf-8");
1284 $this->initTwitterRss();
1287 header('Content-Type: application/atom+xml; charset=utf-8');
1288 $this->initTwitterAtom();
1291 // TRANS: Client error on an API request with an unsupported data format.
1292 $this->clientError(_('Not a supported data format.'));
1299 function endDocument($type='xml')
1306 // Check for JSONP callback
1307 if (isset($this->callback)) {
1312 $this->endTwitterRss();
1315 $this->endTwitterRss();
1318 // TRANS: Client error on an API request with an unsupported data format.
1319 $this->clientError(_('Not a supported data format.'));
1325 function clientError($msg, $code = 400, $format = null)
1327 $action = $this->trimmed('action');
1328 if ($format === null) {
1329 $format = $this->format;
1332 common_debug("User error '$code' on '$action': $msg", __FILE__);
1334 if (!array_key_exists($code, ClientErrorAction::$status)) {
1338 $status_string = ClientErrorAction::$status[$code];
1340 // Do not emit error header for JSONP
1341 if (!isset($this->callback)) {
1342 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1347 $this->initDocument('xml');
1348 $this->elementStart('hash');
1349 $this->element('error', null, $msg);
1350 $this->element('request', null, $_SERVER['REQUEST_URI']);
1351 $this->elementEnd('hash');
1352 $this->endDocument('xml');
1355 $this->initDocument('json');
1356 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1357 print(json_encode($error_array));
1358 $this->endDocument('json');
1361 header('Content-Type: text/plain; charset=utf-8');
1365 // If user didn't request a useful format, throw a regular client error
1366 throw new ClientException($msg, $code);
1370 function serverError($msg, $code = 500, $content_type = null)
1372 $action = $this->trimmed('action');
1373 if ($content_type === null) {
1374 $content_type = $this->format;
1377 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1379 if (!array_key_exists($code, ServerErrorAction::$status)) {
1383 $status_string = ServerErrorAction::$status[$code];
1385 // Do not emit error header for JSONP
1386 if (!isset($this->callback)) {
1387 header('HTTP/1.1 '.$code.' '.$status_string);
1390 if ($content_type == 'xml') {
1391 $this->initDocument('xml');
1392 $this->elementStart('hash');
1393 $this->element('error', null, $msg);
1394 $this->element('request', null, $_SERVER['REQUEST_URI']);
1395 $this->elementEnd('hash');
1396 $this->endDocument('xml');
1398 $this->initDocument('json');
1399 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1400 print(json_encode($error_array));
1401 $this->endDocument('json');
1405 function initTwitterRss()
1408 $this->elementStart(
1412 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1413 'xmlns:georss' => 'http://www.georss.org/georss'
1416 $this->elementStart('channel');
1417 Event::handle('StartApiRss', array($this));
1420 function endTwitterRss()
1422 $this->elementEnd('channel');
1423 $this->elementEnd('rss');
1427 function initTwitterAtom()
1430 // FIXME: don't hardcode the language here!
1431 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1432 'xml:lang' => 'en-US',
1433 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1436 function endTwitterAtom()
1438 $this->elementEnd('feed');
1442 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1444 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1445 switch ($content_type) {
1447 $this->showTwitterXmlUser($profile_array);
1450 $this->showJsonObjects($profile_array);
1453 // TRANS: Client error on an API request with an unsupported data format.
1454 $this->clientError(_('Not a supported data format.'));
1460 private static function is_decimal($str)
1462 return preg_match('/^[0-9]+$/', $str);
1465 function getTargetUser($id)
1468 // Twitter supports these other ways of passing the user ID
1469 if (self::is_decimal($this->arg('id'))) {
1470 return User::staticGet($this->arg('id'));
1471 } else if ($this->arg('id')) {
1472 $nickname = common_canonical_nickname($this->arg('id'));
1473 return User::staticGet('nickname', $nickname);
1474 } else if ($this->arg('user_id')) {
1475 // This is to ensure that a non-numeric user_id still
1476 // overrides screen_name even if it doesn't get used
1477 if (self::is_decimal($this->arg('user_id'))) {
1478 return User::staticGet('id', $this->arg('user_id'));
1480 } else if ($this->arg('screen_name')) {
1481 $nickname = common_canonical_nickname($this->arg('screen_name'));
1482 return User::staticGet('nickname', $nickname);
1484 // Fall back to trying the currently authenticated user
1485 return $this->auth_user;
1488 } else if (self::is_decimal($id)) {
1489 return User::staticGet($id);
1491 $nickname = common_canonical_nickname($id);
1492 return User::staticGet('nickname', $nickname);
1496 function getTargetProfile($id)
1500 // Twitter supports these other ways of passing the user ID
1501 if (self::is_decimal($this->arg('id'))) {
1502 return Profile::staticGet($this->arg('id'));
1503 } else if ($this->arg('id')) {
1504 // Screen names currently can only uniquely identify a local user.
1505 $nickname = common_canonical_nickname($this->arg('id'));
1506 $user = User::staticGet('nickname', $nickname);
1507 return $user ? $user->getProfile() : null;
1508 } else if ($this->arg('user_id')) {
1509 // This is to ensure that a non-numeric user_id still
1510 // overrides screen_name even if it doesn't get used
1511 if (self::is_decimal($this->arg('user_id'))) {
1512 return Profile::staticGet('id', $this->arg('user_id'));
1514 } else if ($this->arg('screen_name')) {
1515 $nickname = common_canonical_nickname($this->arg('screen_name'));
1516 $user = User::staticGet('nickname', $nickname);
1517 return $user ? $user->getProfile() : null;
1519 } else if (self::is_decimal($id)) {
1520 return Profile::staticGet($id);
1522 $nickname = common_canonical_nickname($id);
1523 $user = User::staticGet('nickname', $nickname);
1524 return $user ? $user->getProfile() : null;
1528 function getTargetGroup($id)
1531 if (self::is_decimal($this->arg('id'))) {
1532 return User_group::staticGet('id', $this->arg('id'));
1533 } else if ($this->arg('id')) {
1534 return User_group::getForNickname($this->arg('id'));
1535 } else if ($this->arg('group_id')) {
1536 // This is to ensure that a non-numeric group_id still
1537 // overrides group_name even if it doesn't get used
1538 if (self::is_decimal($this->arg('group_id'))) {
1539 return User_group::staticGet('id', $this->arg('group_id'));
1541 } else if ($this->arg('group_name')) {
1542 return User_group::getForNickname($this->arg('group_name'));
1545 } else if (self::is_decimal($id)) {
1546 return User_group::staticGet('id', $id);
1548 return User_group::getForNickname($id);
1552 function getTargetList($user=null, $id=null)
1554 $tagger = $this->getTargetUser($user);
1558 $id = $this->arg('id');
1562 if (is_numeric($id)) {
1563 $list = Profile_list::staticGet('id', $id);
1565 // only if the list with the id belongs to the tagger
1566 if(empty($list) || $list->tagger != $tagger->id) {
1571 $tag = common_canonical_tag($id);
1572 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1575 if (!empty($list) && $list->private) {
1576 if ($this->auth_user->id == $list->tagger) {
1587 * Returns query argument or default value if not found. Certain
1588 * parameters used throughout the API are lightly scrubbed and
1589 * bounds checked. This overrides Action::arg().
1591 * @param string $key requested argument
1592 * @param string $def default value to return if $key is not provided
1596 function arg($key, $def=null)
1598 // XXX: Do even more input validation/scrubbing?
1600 if (array_key_exists($key, $this->args)) {
1603 $page = (int)$this->args['page'];
1604 return ($page < 1) ? 1 : $page;
1606 $count = (int)$this->args['count'];
1609 } elseif ($count > 200) {
1615 $since_id = (int)$this->args['since_id'];
1616 return ($since_id < 1) ? 0 : $since_id;
1618 $max_id = (int)$this->args['max_id'];
1619 return ($max_id < 1) ? 0 : $max_id;
1621 return parent::arg($key, $def);
1629 * Calculate the complete URI that called up this action. Used for
1630 * Atom rel="self" links. Warning: this is funky.
1632 * @return string URL a URL suitable for rel="self" Atom links
1634 function getSelfUri()
1636 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1638 $id = $this->arg('id');
1639 $aargs = array('format' => $this->format);
1644 $tag = $this->arg('tag');
1646 $aargs['tag'] = $tag;
1649 parse_str($_SERVER['QUERY_STRING'], $params);
1651 if (!empty($params)) {
1652 unset($params['p']);
1653 $pstring = http_build_query($params);
1656 $uri = common_local_url($action, $aargs);
1658 if (!empty($pstring)) {
1659 $uri .= '?' . $pstring;