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();
206 $user = $profile->getUser();
207 } catch (NoSuchUserException $e) {
211 $twitter_user['id'] = intval($profile->id);
212 $twitter_user['name'] = $profile->getBestName();
213 $twitter_user['screen_name'] = $profile->nickname;
214 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
215 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
217 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
218 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
219 Avatar::defaultImage(AVATAR_STREAM_SIZE);
221 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
222 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
223 $twitter_user['followers_count'] = $profile->subscriberCount();
225 // Note: some profiles don't have an associated user
227 $twitter_user['friends_count'] = $profile->subscriptionCount();
229 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
231 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
235 if (!empty($user) && $user->timezone) {
236 $timezone = $user->timezone;
240 $t->setTimezone(new DateTimeZone($timezone));
242 $twitter_user['utc_offset'] = $t->format('Z');
243 $twitter_user['time_zone'] = $timezone;
244 $twitter_user['statuses_count'] = $profile->noticeCount();
246 // Is the requesting user following this user?
247 $twitter_user['following'] = false;
248 $twitter_user['statusnet_blocking'] = false;
249 $twitter_user['notifications'] = false;
251 if (isset($this->auth_user)) {
253 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
254 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
257 $sub = Subscription::pkeyGet(array('subscriber' =>
258 $this->auth_user->id,
259 'subscribed' => $profile->id));
262 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
267 $notice = $profile->getCurrentNotice();
270 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
274 // StatusNet-specific
276 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
278 return $twitter_user;
281 function twitterStatusArray($notice, $include_user=true)
283 $base = $this->twitterSimpleStatusArray($notice, $include_user);
285 if (!empty($notice->repeat_of)) {
286 $original = Notice::getKV('id', $notice->repeat_of);
287 if (!empty($original)) {
288 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
289 $base['retweeted_status'] = $original_array;
296 function twitterSimpleStatusArray($notice, $include_user=true)
298 $profile = $notice->getProfile();
300 $twitter_status = array();
301 $twitter_status['text'] = $notice->content;
302 $twitter_status['truncated'] = false; # Not possible on StatusNet
303 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
304 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
305 intval($notice->reply_to) : null;
309 $ns = $notice->getSource();
311 if (!empty($ns->name) && !empty($ns->url)) {
312 $source = '<a href="'
313 . htmlspecialchars($ns->url)
314 . '" rel="nofollow">'
315 . htmlspecialchars($ns->name)
322 $twitter_status['source'] = $source;
323 $twitter_status['id'] = intval($notice->id);
325 $replier_profile = null;
327 if ($notice->reply_to) {
328 $reply = Notice::getKV(intval($notice->reply_to));
330 $replier_profile = $reply->getProfile();
334 $twitter_status['in_reply_to_user_id'] =
335 ($replier_profile) ? intval($replier_profile->id) : null;
336 $twitter_status['in_reply_to_screen_name'] =
337 ($replier_profile) ? $replier_profile->nickname : null;
339 if (isset($notice->lat) && isset($notice->lon)) {
340 // This is the format that GeoJSON expects stuff to be in
341 $twitter_status['geo'] = array('type' => 'Point',
342 'coordinates' => array((float) $notice->lat,
343 (float) $notice->lon));
345 $twitter_status['geo'] = null;
348 if (isset($this->auth_user)) {
349 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
351 $twitter_status['favorited'] = false;
355 $attachments = $notice->attachments();
357 if (!empty($attachments)) {
359 $twitter_status['attachments'] = array();
361 foreach ($attachments as $attachment) {
362 $enclosure_o=$attachment->getEnclosure();
364 $enclosure = array();
365 $enclosure['url'] = $enclosure_o->url;
366 $enclosure['mimetype'] = $enclosure_o->mimetype;
367 $enclosure['size'] = $enclosure_o->size;
368 $twitter_status['attachments'][] = $enclosure;
373 if ($include_user && $profile) {
374 // Don't get notice (recursive!)
375 $twitter_user = $this->twitterUserArray($profile, false);
376 $twitter_status['user'] = $twitter_user;
379 // StatusNet-specific
381 $twitter_status['statusnet_html'] = $notice->rendered;
382 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
384 return $twitter_status;
387 function twitterGroupArray($group)
389 $twitter_group = array();
391 $twitter_group['id'] = intval($group->id);
392 $twitter_group['url'] = $group->permalink();
393 $twitter_group['nickname'] = $group->nickname;
394 $twitter_group['fullname'] = $group->fullname;
396 if (isset($this->auth_user)) {
397 $twitter_group['member'] = $this->auth_user->isMember($group);
398 $twitter_group['blocked'] = Group_block::isBlocked(
400 $this->auth_user->getProfile()
404 $twitter_group['member_count'] = $group->getMemberCount();
405 $twitter_group['original_logo'] = $group->original_logo;
406 $twitter_group['homepage_logo'] = $group->homepage_logo;
407 $twitter_group['stream_logo'] = $group->stream_logo;
408 $twitter_group['mini_logo'] = $group->mini_logo;
409 $twitter_group['homepage'] = $group->homepage;
410 $twitter_group['description'] = $group->description;
411 $twitter_group['location'] = $group->location;
412 $twitter_group['created'] = $this->dateTwitter($group->created);
413 $twitter_group['modified'] = $this->dateTwitter($group->modified);
415 return $twitter_group;
418 function twitterRssGroupArray($group)
421 $entry['content']=$group->description;
422 $entry['title']=$group->nickname;
423 $entry['link']=$group->permalink();
424 $entry['published']=common_date_iso8601($group->created);
425 $entry['updated']==common_date_iso8601($group->modified);
426 $taguribase = common_config('integration', 'groupuri');
427 $entry['id'] = "group:$groupuribase:$entry[link]";
429 $entry['description'] = $entry['content'];
430 $entry['pubDate'] = common_date_rfc2822($group->created);
431 $entry['guid'] = $entry['link'];
436 function twitterListArray($list)
438 $profile = Profile::getKV('id', $list->tagger);
440 $twitter_list = array();
441 $twitter_list['id'] = $list->id;
442 $twitter_list['name'] = $list->tag;
443 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
444 $twitter_list['slug'] = $list->tag;
445 $twitter_list['description'] = $list->description;
446 $twitter_list['subscriber_count'] = $list->subscriberCount();
447 $twitter_list['member_count'] = $list->taggedCount();
448 $twitter_list['uri'] = $list->getUri();
450 if (isset($this->auth_user)) {
451 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
453 $twitter_list['following'] = false;
456 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
457 $twitter_list['user'] = $this->twitterUserArray($profile, false);
459 return $twitter_list;
462 function twitterRssEntryArray($notice)
466 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
467 $profile = $notice->getProfile();
469 // We trim() to avoid extraneous whitespace in the output
471 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
472 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
473 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
474 $entry['published'] = common_date_iso8601($notice->created);
476 $taguribase = TagURI::base();
477 $entry['id'] = "tag:$taguribase:$entry[link]";
479 $entry['updated'] = $entry['published'];
480 $entry['author'] = $profile->getBestName();
483 $attachments = $notice->attachments();
484 $enclosures = array();
486 foreach ($attachments as $attachment) {
487 $enclosure_o=$attachment->getEnclosure();
489 $enclosure = array();
490 $enclosure['url'] = $enclosure_o->url;
491 $enclosure['mimetype'] = $enclosure_o->mimetype;
492 $enclosure['size'] = $enclosure_o->size;
493 $enclosures[] = $enclosure;
497 if (!empty($enclosures)) {
498 $entry['enclosures'] = $enclosures;
502 $tag = new Notice_tag();
503 $tag->notice_id = $notice->id;
505 $entry['tags']=array();
506 while ($tag->fetch()) {
507 $entry['tags'][]=$tag->tag;
513 $entry['description'] = $entry['content'];
514 $entry['pubDate'] = common_date_rfc2822($notice->created);
515 $entry['guid'] = $entry['link'];
517 if (isset($notice->lat) && isset($notice->lon)) {
518 // This is the format that GeoJSON expects stuff to be in.
519 // showGeoRSS() below uses it for XML output, so we reuse it
520 $entry['geo'] = array('type' => 'Point',
521 'coordinates' => array((float) $notice->lat,
522 (float) $notice->lon));
524 $entry['geo'] = null;
527 Event::handle('EndRssEntryArray', array($notice, &$entry));
533 function twitterRelationshipArray($source, $target)
535 $relationship = array();
537 $relationship['source'] =
538 $this->relationshipDetailsArray($source, $target);
539 $relationship['target'] =
540 $this->relationshipDetailsArray($target, $source);
542 return array('relationship' => $relationship);
545 function relationshipDetailsArray($source, $target)
549 $details['screen_name'] = $source->nickname;
550 $details['followed_by'] = $target->isSubscribed($source);
551 $details['following'] = $source->isSubscribed($target);
553 $notifications = false;
555 if ($source->isSubscribed($target)) {
556 $sub = Subscription::pkeyGet(array('subscriber' =>
557 $source->id, 'subscribed' => $target->id));
560 $notifications = ($sub->jabber || $sub->sms);
564 $details['notifications_enabled'] = $notifications;
565 $details['blocking'] = $source->hasBlocked($target);
566 $details['id'] = intval($source->id);
571 function showTwitterXmlRelationship($relationship)
573 $this->elementStart('relationship');
575 foreach($relationship as $element => $value) {
576 if ($element == 'source' || $element == 'target') {
577 $this->elementStart($element);
578 $this->showXmlRelationshipDetails($value);
579 $this->elementEnd($element);
583 $this->elementEnd('relationship');
586 function showXmlRelationshipDetails($details)
588 foreach($details as $element => $value) {
589 $this->element($element, null, $value);
593 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
597 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
599 $this->elementStart($tag, $attrs);
600 foreach($twitter_status as $element => $value) {
603 $this->showTwitterXmlUser($twitter_status['user']);
606 $this->element($element, null, common_xml_safe_str($value));
609 $this->showXmlAttachments($twitter_status['attachments']);
612 $this->showGeoXML($value);
614 case 'retweeted_status':
615 $this->showTwitterXmlStatus($value, 'retweeted_status');
618 if (strncmp($element, 'statusnet_', 10) == 0) {
619 $this->element('statusnet:'.substr($element, 10), null, $value);
621 $this->element($element, null, $value);
625 $this->elementEnd($tag);
628 function showTwitterXmlGroup($twitter_group)
630 $this->elementStart('group');
631 foreach($twitter_group as $element => $value) {
632 $this->element($element, null, $value);
634 $this->elementEnd('group');
637 function showTwitterXmlList($twitter_list)
639 $this->elementStart('list');
640 foreach($twitter_list as $element => $value) {
641 if($element == 'user') {
642 $this->showTwitterXmlUser($value, 'user');
645 $this->element($element, null, $value);
648 $this->elementEnd('list');
651 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
655 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
657 $this->elementStart($role, $attrs);
658 foreach($twitter_user as $element => $value) {
659 if ($element == 'status') {
660 $this->showTwitterXmlStatus($twitter_user['status']);
661 } else if (strncmp($element, 'statusnet_', 10) == 0) {
662 $this->element('statusnet:'.substr($element, 10), null, $value);
664 $this->element($element, null, $value);
667 $this->elementEnd($role);
670 function showXmlAttachments($attachments) {
671 if (!empty($attachments)) {
672 $this->elementStart('attachments', array('type' => 'array'));
673 foreach ($attachments as $attachment) {
675 $attrs['url'] = $attachment['url'];
676 $attrs['mimetype'] = $attachment['mimetype'];
677 $attrs['size'] = $attachment['size'];
678 $this->element('enclosure', $attrs, '');
680 $this->elementEnd('attachments');
684 function showGeoXML($geo)
688 $this->element('geo');
690 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
691 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
692 $this->elementEnd('geo');
696 function showGeoRSS($geo)
702 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
707 function showTwitterRssItem($entry)
709 $this->elementStart('item');
710 $this->element('title', null, $entry['title']);
711 $this->element('description', null, $entry['description']);
712 $this->element('pubDate', null, $entry['pubDate']);
713 $this->element('guid', null, $entry['guid']);
714 $this->element('link', null, $entry['link']);
716 // RSS only supports 1 enclosure per item
717 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
718 $enclosure = $entry['enclosures'][0];
719 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
722 if(array_key_exists('tags', $entry)){
723 foreach($entry['tags'] as $tag){
724 $this->element('category', null,$tag);
728 $this->showGeoRSS($entry['geo']);
729 $this->elementEnd('item');
732 function showJsonObjects($objects)
734 print(json_encode($objects));
737 function showSingleXmlStatus($notice)
739 $this->initDocument('xml');
740 $twitter_status = $this->twitterStatusArray($notice);
741 $this->showTwitterXmlStatus($twitter_status, 'status', true);
742 $this->endDocument('xml');
745 function showSingleAtomStatus($notice)
747 header('Content-Type: application/atom+xml; charset=utf-8');
748 print $notice->asAtomEntry(true, true, true, $this->auth_user);
751 function show_single_json_status($notice)
753 $this->initDocument('json');
754 $status = $this->twitterStatusArray($notice);
755 $this->showJsonObjects($status);
756 $this->endDocument('json');
759 function showXmlTimeline($notice)
761 $this->initDocument('xml');
762 $this->elementStart('statuses', array('type' => 'array',
763 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
765 if (is_array($notice)) {
766 $notice = new ArrayWrapper($notice);
769 while ($notice->fetch()) {
771 $twitter_status = $this->twitterStatusArray($notice);
772 $this->showTwitterXmlStatus($twitter_status);
773 } catch (Exception $e) {
774 common_log(LOG_ERR, $e->getMessage());
779 $this->elementEnd('statuses');
780 $this->endDocument('xml');
783 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
785 $this->initDocument('rss');
787 $this->element('title', null, $title);
788 $this->element('link', null, $link);
790 if (!is_null($self)) {
794 'type' => 'application/rss+xml',
801 if (!is_null($suplink)) {
802 // For FriendFeed's SUP protocol
803 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
804 'rel' => 'http://api.friendfeed.com/2008/03#sup',
806 'type' => 'application/json'));
809 if (!is_null($logo)) {
810 $this->elementStart('image');
811 $this->element('link', null, $link);
812 $this->element('title', null, $title);
813 $this->element('url', null, $logo);
814 $this->elementEnd('image');
817 $this->element('description', null, $subtitle);
818 $this->element('language', null, 'en-us');
819 $this->element('ttl', null, '40');
821 if (is_array($notice)) {
822 $notice = new ArrayWrapper($notice);
825 while ($notice->fetch()) {
827 $entry = $this->twitterRssEntryArray($notice);
828 $this->showTwitterRssItem($entry);
829 } catch (Exception $e) {
830 common_log(LOG_ERR, $e->getMessage());
831 // continue on exceptions
835 $this->endTwitterRss();
838 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
840 $this->initDocument('atom');
842 $this->element('title', null, $title);
843 $this->element('id', null, $id);
844 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
846 if (!is_null($logo)) {
847 $this->element('logo',null,$logo);
850 if (!is_null($suplink)) {
851 // For FriendFeed's SUP protocol
852 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
854 'type' => 'application/json'));
857 if (!is_null($selfuri)) {
858 $this->element('link', array('href' => $selfuri,
859 'rel' => 'self', 'type' => 'application/atom+xml'), null);
862 $this->element('updated', null, common_date_iso8601('now'));
863 $this->element('subtitle', null, $subtitle);
865 if (is_array($notice)) {
866 $notice = new ArrayWrapper($notice);
869 while ($notice->fetch()) {
871 $this->raw($notice->asAtomEntry());
872 } catch (Exception $e) {
873 common_log(LOG_ERR, $e->getMessage());
878 $this->endDocument('atom');
881 function showRssGroups($group, $title, $link, $subtitle)
883 $this->initDocument('rss');
885 $this->element('title', null, $title);
886 $this->element('link', null, $link);
887 $this->element('description', null, $subtitle);
888 $this->element('language', null, 'en-us');
889 $this->element('ttl', null, '40');
891 if (is_array($group)) {
892 foreach ($group as $g) {
893 $twitter_group = $this->twitterRssGroupArray($g);
894 $this->showTwitterRssItem($twitter_group);
897 while ($group->fetch()) {
898 $twitter_group = $this->twitterRssGroupArray($group);
899 $this->showTwitterRssItem($twitter_group);
903 $this->endTwitterRss();
906 function showTwitterAtomEntry($entry)
908 $this->elementStart('entry');
909 $this->element('title', null, common_xml_safe_str($entry['title']));
912 array('type' => 'html'),
913 common_xml_safe_str($entry['content'])
915 $this->element('id', null, $entry['id']);
916 $this->element('published', null, $entry['published']);
917 $this->element('updated', null, $entry['updated']);
918 $this->element('link', array('type' => 'text/html',
919 'href' => $entry['link'],
920 'rel' => 'alternate'));
921 $this->element('link', array('type' => $entry['avatar-type'],
922 'href' => $entry['avatar'],
924 $this->elementStart('author');
926 $this->element('name', null, $entry['author-name']);
927 $this->element('uri', null, $entry['author-uri']);
929 $this->elementEnd('author');
930 $this->elementEnd('entry');
933 function showXmlDirectMessage($dm, $namespaces=false)
937 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
939 $this->elementStart('direct_message', $attrs);
940 foreach($dm as $element => $value) {
944 $this->showTwitterXmlUser($value, $element);
947 $this->element($element, null, common_xml_safe_str($value));
950 $this->element($element, null, $value);
954 $this->elementEnd('direct_message');
957 function directMessageArray($message)
961 $from_profile = $message->getFrom();
962 $to_profile = $message->getTo();
964 $dmsg['id'] = intval($message->id);
965 $dmsg['sender_id'] = intval($from_profile->id);
966 $dmsg['text'] = trim($message->content);
967 $dmsg['recipient_id'] = intval($to_profile->id);
968 $dmsg['created_at'] = $this->dateTwitter($message->created);
969 $dmsg['sender_screen_name'] = $from_profile->nickname;
970 $dmsg['recipient_screen_name'] = $to_profile->nickname;
971 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
972 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
977 function rssDirectMessageArray($message)
981 $from = $message->getFrom();
983 $entry['title'] = sprintf('Message from %1$s to %2$s',
984 $from->nickname, $message->getTo()->nickname);
986 $entry['content'] = common_xml_safe_str($message->rendered);
987 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
988 $entry['published'] = common_date_iso8601($message->created);
990 $taguribase = TagURI::base();
992 $entry['id'] = "tag:$taguribase:$entry[link]";
993 $entry['updated'] = $entry['published'];
995 $entry['author-name'] = $from->getBestName();
996 $entry['author-uri'] = $from->homepage;
998 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1000 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
1001 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
1003 // RSS item specific
1005 $entry['description'] = $entry['content'];
1006 $entry['pubDate'] = common_date_rfc2822($message->created);
1007 $entry['guid'] = $entry['link'];
1012 function showSingleXmlDirectMessage($message)
1014 $this->initDocument('xml');
1015 $dmsg = $this->directMessageArray($message);
1016 $this->showXmlDirectMessage($dmsg, true);
1017 $this->endDocument('xml');
1020 function showSingleJsonDirectMessage($message)
1022 $this->initDocument('json');
1023 $dmsg = $this->directMessageArray($message);
1024 $this->showJsonObjects($dmsg);
1025 $this->endDocument('json');
1028 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1030 $this->initDocument('atom');
1032 $this->element('title', null, common_xml_safe_str($title));
1033 $this->element('id', null, $id);
1034 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1036 if (!is_null($selfuri)) {
1037 $this->element('link', array('href' => $selfuri,
1038 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1041 $this->element('updated', null, common_date_iso8601('now'));
1042 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1044 if (is_array($group)) {
1045 foreach ($group as $g) {
1046 $this->raw($g->asAtomEntry());
1049 while ($group->fetch()) {
1050 $this->raw($group->asAtomEntry());
1054 $this->endDocument('atom');
1058 function showJsonTimeline($notice)
1060 $this->initDocument('json');
1062 $statuses = array();
1064 if (is_array($notice)) {
1065 $notice = new ArrayWrapper($notice);
1068 while ($notice->fetch()) {
1070 $twitter_status = $this->twitterStatusArray($notice);
1071 array_push($statuses, $twitter_status);
1072 } catch (Exception $e) {
1073 common_log(LOG_ERR, $e->getMessage());
1078 $this->showJsonObjects($statuses);
1080 $this->endDocument('json');
1083 function showJsonGroups($group)
1085 $this->initDocument('json');
1089 if (is_array($group)) {
1090 foreach ($group as $g) {
1091 $twitter_group = $this->twitterGroupArray($g);
1092 array_push($groups, $twitter_group);
1095 while ($group->fetch()) {
1096 $twitter_group = $this->twitterGroupArray($group);
1097 array_push($groups, $twitter_group);
1101 $this->showJsonObjects($groups);
1103 $this->endDocument('json');
1106 function showXmlGroups($group)
1109 $this->initDocument('xml');
1110 $this->elementStart('groups', array('type' => 'array'));
1112 if (is_array($group)) {
1113 foreach ($group as $g) {
1114 $twitter_group = $this->twitterGroupArray($g);
1115 $this->showTwitterXmlGroup($twitter_group);
1118 while ($group->fetch()) {
1119 $twitter_group = $this->twitterGroupArray($group);
1120 $this->showTwitterXmlGroup($twitter_group);
1124 $this->elementEnd('groups');
1125 $this->endDocument('xml');
1128 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1131 $this->initDocument('xml');
1132 $this->elementStart('lists_list');
1133 $this->elementStart('lists', array('type' => 'array'));
1135 if (is_array($list)) {
1136 foreach ($list as $l) {
1137 $twitter_list = $this->twitterListArray($l);
1138 $this->showTwitterXmlList($twitter_list);
1141 while ($list->fetch()) {
1142 $twitter_list = $this->twitterListArray($list);
1143 $this->showTwitterXmlList($twitter_list);
1147 $this->elementEnd('lists');
1149 $this->element('next_cursor', null, $next_cursor);
1150 $this->element('previous_cursor', null, $prev_cursor);
1152 $this->elementEnd('lists_list');
1153 $this->endDocument('xml');
1156 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1158 $this->initDocument('json');
1162 if (is_array($list)) {
1163 foreach ($list as $l) {
1164 $twitter_list = $this->twitterListArray($l);
1165 array_push($lists, $twitter_list);
1168 while ($list->fetch()) {
1169 $twitter_list = $this->twitterListArray($list);
1170 array_push($lists, $twitter_list);
1174 $lists_list = array(
1176 'next_cursor' => $next_cursor,
1177 'next_cursor_str' => strval($next_cursor),
1178 'previous_cursor' => $prev_cursor,
1179 'previous_cursor_str' => strval($prev_cursor)
1182 $this->showJsonObjects($lists_list);
1184 $this->endDocument('json');
1187 function showTwitterXmlUsers($user)
1189 $this->initDocument('xml');
1190 $this->elementStart('users', array('type' => 'array',
1191 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1193 if (is_array($user)) {
1194 foreach ($user as $u) {
1195 $twitter_user = $this->twitterUserArray($u);
1196 $this->showTwitterXmlUser($twitter_user);
1199 while ($user->fetch()) {
1200 $twitter_user = $this->twitterUserArray($user);
1201 $this->showTwitterXmlUser($twitter_user);
1205 $this->elementEnd('users');
1206 $this->endDocument('xml');
1209 function showJsonUsers($user)
1211 $this->initDocument('json');
1215 if (is_array($user)) {
1216 foreach ($user as $u) {
1217 $twitter_user = $this->twitterUserArray($u);
1218 array_push($users, $twitter_user);
1221 while ($user->fetch()) {
1222 $twitter_user = $this->twitterUserArray($user);
1223 array_push($users, $twitter_user);
1227 $this->showJsonObjects($users);
1229 $this->endDocument('json');
1232 function showSingleJsonGroup($group)
1234 $this->initDocument('json');
1235 $twitter_group = $this->twitterGroupArray($group);
1236 $this->showJsonObjects($twitter_group);
1237 $this->endDocument('json');
1240 function showSingleXmlGroup($group)
1242 $this->initDocument('xml');
1243 $twitter_group = $this->twitterGroupArray($group);
1244 $this->showTwitterXmlGroup($twitter_group);
1245 $this->endDocument('xml');
1248 function showSingleJsonList($list)
1250 $this->initDocument('json');
1251 $twitter_list = $this->twitterListArray($list);
1252 $this->showJsonObjects($twitter_list);
1253 $this->endDocument('json');
1256 function showSingleXmlList($list)
1258 $this->initDocument('xml');
1259 $twitter_list = $this->twitterListArray($list);
1260 $this->showTwitterXmlList($twitter_list);
1261 $this->endDocument('xml');
1264 function dateTwitter($dt)
1266 $dateStr = date('d F Y H:i:s', strtotime($dt));
1267 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1268 $d->setTimezone(new DateTimeZone(common_timezone()));
1269 return $d->format('D M d H:i:s O Y');
1272 function initDocument($type='xml')
1276 header('Content-Type: application/xml; charset=utf-8');
1280 header('Content-Type: application/json; charset=utf-8');
1282 // Check for JSONP callback
1283 if (isset($this->callback)) {
1284 print $this->callback . '(';
1288 header("Content-Type: application/rss+xml; charset=utf-8");
1289 $this->initTwitterRss();
1292 header('Content-Type: application/atom+xml; charset=utf-8');
1293 $this->initTwitterAtom();
1296 // TRANS: Client error on an API request with an unsupported data format.
1297 $this->clientError(_('Not a supported data format.'));
1304 function endDocument($type='xml')
1311 // Check for JSONP callback
1312 if (isset($this->callback)) {
1317 $this->endTwitterRss();
1320 $this->endTwitterRss();
1323 // TRANS: Client error on an API request with an unsupported data format.
1324 $this->clientError(_('Not a supported data format.'));
1330 function clientError($msg, $code = 400, $format = null)
1332 $action = $this->trimmed('action');
1333 if ($format === null) {
1334 $format = $this->format;
1337 common_debug("User error '$code' on '$action': $msg", __FILE__);
1339 if (!array_key_exists($code, ClientErrorAction::$status)) {
1343 $status_string = ClientErrorAction::$status[$code];
1345 // Do not emit error header for JSONP
1346 if (!isset($this->callback)) {
1347 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1352 $this->initDocument('xml');
1353 $this->elementStart('hash');
1354 $this->element('error', null, $msg);
1355 $this->element('request', null, $_SERVER['REQUEST_URI']);
1356 $this->elementEnd('hash');
1357 $this->endDocument('xml');
1360 $this->initDocument('json');
1361 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1362 print(json_encode($error_array));
1363 $this->endDocument('json');
1366 header('Content-Type: text/plain; charset=utf-8');
1370 // If user didn't request a useful format, throw a regular client error
1371 throw new ClientException($msg, $code);
1375 function serverError($msg, $code = 500, $content_type = null)
1377 $action = $this->trimmed('action');
1378 if ($content_type === null) {
1379 $content_type = $this->format;
1382 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1384 if (!array_key_exists($code, ServerErrorAction::$status)) {
1388 $status_string = ServerErrorAction::$status[$code];
1390 // Do not emit error header for JSONP
1391 if (!isset($this->callback)) {
1392 header('HTTP/1.1 '.$code.' '.$status_string);
1395 if ($content_type == 'xml') {
1396 $this->initDocument('xml');
1397 $this->elementStart('hash');
1398 $this->element('error', null, $msg);
1399 $this->element('request', null, $_SERVER['REQUEST_URI']);
1400 $this->elementEnd('hash');
1401 $this->endDocument('xml');
1403 $this->initDocument('json');
1404 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1405 print(json_encode($error_array));
1406 $this->endDocument('json');
1410 function initTwitterRss()
1413 $this->elementStart(
1417 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1418 'xmlns:georss' => 'http://www.georss.org/georss'
1421 $this->elementStart('channel');
1422 Event::handle('StartApiRss', array($this));
1425 function endTwitterRss()
1427 $this->elementEnd('channel');
1428 $this->elementEnd('rss');
1432 function initTwitterAtom()
1435 // FIXME: don't hardcode the language here!
1436 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1437 'xml:lang' => 'en-US',
1438 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1441 function endTwitterAtom()
1443 $this->elementEnd('feed');
1447 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1449 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1450 switch ($content_type) {
1452 $this->showTwitterXmlUser($profile_array);
1455 $this->showJsonObjects($profile_array);
1458 // TRANS: Client error on an API request with an unsupported data format.
1459 $this->clientError(_('Not a supported data format.'));
1465 private static function is_decimal($str)
1467 return preg_match('/^[0-9]+$/', $str);
1470 function getTargetUser($id)
1473 // Twitter supports these other ways of passing the user ID
1474 if (self::is_decimal($this->arg('id'))) {
1475 return User::getKV($this->arg('id'));
1476 } else if ($this->arg('id')) {
1477 $nickname = common_canonical_nickname($this->arg('id'));
1478 return User::getKV('nickname', $nickname);
1479 } else if ($this->arg('user_id')) {
1480 // This is to ensure that a non-numeric user_id still
1481 // overrides screen_name even if it doesn't get used
1482 if (self::is_decimal($this->arg('user_id'))) {
1483 return User::getKV('id', $this->arg('user_id'));
1485 } else if ($this->arg('screen_name')) {
1486 $nickname = common_canonical_nickname($this->arg('screen_name'));
1487 return User::getKV('nickname', $nickname);
1489 // Fall back to trying the currently authenticated user
1490 return $this->auth_user;
1493 } else if (self::is_decimal($id)) {
1494 return User::getKV($id);
1496 $nickname = common_canonical_nickname($id);
1497 return User::getKV('nickname', $nickname);
1501 function getTargetProfile($id)
1505 // Twitter supports these other ways of passing the user ID
1506 if (self::is_decimal($this->arg('id'))) {
1507 return Profile::getKV($this->arg('id'));
1508 } else if ($this->arg('id')) {
1509 // Screen names currently can only uniquely identify a local user.
1510 $nickname = common_canonical_nickname($this->arg('id'));
1511 $user = User::getKV('nickname', $nickname);
1512 return $user ? $user->getProfile() : null;
1513 } else if ($this->arg('user_id')) {
1514 // This is to ensure that a non-numeric user_id still
1515 // overrides screen_name even if it doesn't get used
1516 if (self::is_decimal($this->arg('user_id'))) {
1517 return Profile::getKV('id', $this->arg('user_id'));
1519 } else if ($this->arg('screen_name')) {
1520 $nickname = common_canonical_nickname($this->arg('screen_name'));
1521 $user = User::getKV('nickname', $nickname);
1522 return $user ? $user->getProfile() : null;
1524 } else if (self::is_decimal($id)) {
1525 return Profile::getKV($id);
1527 $nickname = common_canonical_nickname($id);
1528 $user = User::getKV('nickname', $nickname);
1529 return $user ? $user->getProfile() : null;
1533 function getTargetGroup($id)
1536 if (self::is_decimal($this->arg('id'))) {
1537 return User_group::getKV('id', $this->arg('id'));
1538 } else if ($this->arg('id')) {
1539 return User_group::getForNickname($this->arg('id'));
1540 } else if ($this->arg('group_id')) {
1541 // This is to ensure that a non-numeric group_id still
1542 // overrides group_name even if it doesn't get used
1543 if (self::is_decimal($this->arg('group_id'))) {
1544 return User_group::getKV('id', $this->arg('group_id'));
1546 } else if ($this->arg('group_name')) {
1547 return User_group::getForNickname($this->arg('group_name'));
1550 } else if (self::is_decimal($id)) {
1551 return User_group::getKV('id', $id);
1553 return User_group::getForNickname($id);
1557 function getTargetList($user=null, $id=null)
1559 $tagger = $this->getTargetUser($user);
1563 $id = $this->arg('id');
1567 if (is_numeric($id)) {
1568 $list = Profile_list::getKV('id', $id);
1570 // only if the list with the id belongs to the tagger
1571 if(empty($list) || $list->tagger != $tagger->id) {
1576 $tag = common_canonical_tag($id);
1577 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1580 if (!empty($list) && $list->private) {
1581 if ($this->auth_user->id == $list->tagger) {
1592 * Returns query argument or default value if not found. Certain
1593 * parameters used throughout the API are lightly scrubbed and
1594 * bounds checked. This overrides Action::arg().
1596 * @param string $key requested argument
1597 * @param string $def default value to return if $key is not provided
1601 function arg($key, $def=null)
1603 // XXX: Do even more input validation/scrubbing?
1605 if (array_key_exists($key, $this->args)) {
1608 $page = (int)$this->args['page'];
1609 return ($page < 1) ? 1 : $page;
1611 $count = (int)$this->args['count'];
1614 } elseif ($count > 200) {
1620 $since_id = (int)$this->args['since_id'];
1621 return ($since_id < 1) ? 0 : $since_id;
1623 $max_id = (int)$this->args['max_id'];
1624 return ($max_id < 1) ? 0 : $max_id;
1626 return parent::arg($key, $def);
1634 * Calculate the complete URI that called up this action. Used for
1635 * Atom rel="self" links. Warning: this is funky.
1637 * @return string URL a URL suitable for rel="self" Atom links
1639 function getSelfUri()
1641 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1643 $id = $this->arg('id');
1644 $aargs = array('format' => $this->format);
1649 $tag = $this->arg('tag');
1651 $aargs['tag'] = $tag;
1654 parse_str($_SERVER['QUERY_STRING'], $params);
1656 if (!empty($params)) {
1657 unset($params['p']);
1658 $pstring = http_build_query($params);
1661 $uri = common_local_url($action, $aargs);
1663 if (!empty($pstring)) {
1664 $uri .= '?' . $pstring;