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);
320 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
322 $replier_profile = null;
324 if ($notice->reply_to) {
325 $reply = Notice::staticGet(intval($notice->reply_to));
327 $replier_profile = $reply->getProfile();
331 $twitter_status['in_reply_to_user_id'] =
332 ($replier_profile) ? intval($replier_profile->id) : null;
333 $twitter_status['in_reply_to_screen_name'] =
334 ($replier_profile) ? $replier_profile->nickname : null;
336 if (isset($notice->lat) && isset($notice->lon)) {
337 // This is the format that GeoJSON expects stuff to be in
338 $twitter_status['geo'] = array('type' => 'Point',
339 'coordinates' => array((float) $notice->lat,
340 (float) $notice->lon));
342 $twitter_status['geo'] = null;
345 if (isset($this->auth_user)) {
346 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
348 $twitter_status['favorited'] = false;
352 $attachments = $notice->attachments();
354 if (!empty($attachments)) {
356 $twitter_status['attachments'] = array();
358 foreach ($attachments as $attachment) {
359 $enclosure_o=$attachment->getEnclosure();
361 $enclosure = array();
362 $enclosure['url'] = $enclosure_o->url;
363 $enclosure['mimetype'] = $enclosure_o->mimetype;
364 $enclosure['size'] = $enclosure_o->size;
365 $twitter_status['attachments'][] = $enclosure;
370 if ($include_user && $profile) {
371 // Don't get notice (recursive!)
372 $twitter_user = $this->twitterUserArray($profile, false);
373 $twitter_status['user'] = $twitter_user;
376 // StatusNet-specific
378 $twitter_status['statusnet_html'] = $notice->rendered;
379 $twitter_status['statusnet_conversation_id'] = $notice->conversation;
381 return $twitter_status;
384 function twitterGroupArray($group)
386 $twitter_group = array();
388 $twitter_group['id'] = intval($group->id);
389 $twitter_group['url'] = $group->permalink();
390 $twitter_group['nickname'] = $group->nickname;
391 $twitter_group['fullname'] = $group->fullname;
393 if (isset($this->auth_user)) {
394 $twitter_group['member'] = $this->auth_user->isMember($group);
395 $twitter_group['blocked'] = Group_block::isBlocked(
397 $this->auth_user->getProfile()
401 $twitter_group['member_count'] = $group->getMemberCount();
402 $twitter_group['original_logo'] = $group->original_logo;
403 $twitter_group['homepage_logo'] = $group->homepage_logo;
404 $twitter_group['stream_logo'] = $group->stream_logo;
405 $twitter_group['mini_logo'] = $group->mini_logo;
406 $twitter_group['homepage'] = $group->homepage;
407 $twitter_group['description'] = $group->description;
408 $twitter_group['location'] = $group->location;
409 $twitter_group['created'] = $this->dateTwitter($group->created);
410 $twitter_group['modified'] = $this->dateTwitter($group->modified);
412 return $twitter_group;
415 function twitterRssGroupArray($group)
418 $entry['content']=$group->description;
419 $entry['title']=$group->nickname;
420 $entry['link']=$group->permalink();
421 $entry['published']=common_date_iso8601($group->created);
422 $entry['updated']==common_date_iso8601($group->modified);
423 $taguribase = common_config('integration', 'groupuri');
424 $entry['id'] = "group:$groupuribase:$entry[link]";
426 $entry['description'] = $entry['content'];
427 $entry['pubDate'] = common_date_rfc2822($group->created);
428 $entry['guid'] = $entry['link'];
433 function twitterListArray($list)
435 $profile = Profile::staticGet('id', $list->tagger);
437 $twitter_list = array();
438 $twitter_list['id'] = $list->id;
439 $twitter_list['name'] = $list->tag;
440 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
441 $twitter_list['slug'] = $list->tag;
442 $twitter_list['description'] = $list->description;
443 $twitter_list['subscriber_count'] = $list->subscriberCount();
444 $twitter_list['member_count'] = $list->taggedCount();
445 $twitter_list['uri'] = $list->getUri();
447 if (isset($this->auth_user)) {
448 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
450 $twitter_list['following'] = false;
453 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
454 $twitter_list['user'] = $this->twitterUserArray($profile, false);
456 return $twitter_list;
459 function twitterRssEntryArray($notice)
463 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
464 $profile = $notice->getProfile();
466 // We trim() to avoid extraneous whitespace in the output
468 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
469 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
470 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
471 $entry['published'] = common_date_iso8601($notice->created);
473 $taguribase = TagURI::base();
474 $entry['id'] = "tag:$taguribase:$entry[link]";
476 $entry['updated'] = $entry['published'];
477 $entry['author'] = $profile->getBestName();
480 $attachments = $notice->attachments();
481 $enclosures = array();
483 foreach ($attachments as $attachment) {
484 $enclosure_o=$attachment->getEnclosure();
486 $enclosure = array();
487 $enclosure['url'] = $enclosure_o->url;
488 $enclosure['mimetype'] = $enclosure_o->mimetype;
489 $enclosure['size'] = $enclosure_o->size;
490 $enclosures[] = $enclosure;
494 if (!empty($enclosures)) {
495 $entry['enclosures'] = $enclosures;
499 $tag = new Notice_tag();
500 $tag->notice_id = $notice->id;
502 $entry['tags']=array();
503 while ($tag->fetch()) {
504 $entry['tags'][]=$tag->tag;
510 $entry['description'] = $entry['content'];
511 $entry['pubDate'] = common_date_rfc2822($notice->created);
512 $entry['guid'] = $entry['link'];
514 if (isset($notice->lat) && isset($notice->lon)) {
515 // This is the format that GeoJSON expects stuff to be in.
516 // showGeoRSS() below uses it for XML output, so we reuse it
517 $entry['geo'] = array('type' => 'Point',
518 'coordinates' => array((float) $notice->lat,
519 (float) $notice->lon));
521 $entry['geo'] = null;
524 Event::handle('EndRssEntryArray', array($notice, &$entry));
530 function twitterRelationshipArray($source, $target)
532 $relationship = array();
534 $relationship['source'] =
535 $this->relationshipDetailsArray($source, $target);
536 $relationship['target'] =
537 $this->relationshipDetailsArray($target, $source);
539 return array('relationship' => $relationship);
542 function relationshipDetailsArray($source, $target)
546 $details['screen_name'] = $source->nickname;
547 $details['followed_by'] = $target->isSubscribed($source);
548 $details['following'] = $source->isSubscribed($target);
550 $notifications = false;
552 if ($source->isSubscribed($target)) {
553 $sub = Subscription::pkeyGet(array('subscriber' =>
554 $source->id, 'subscribed' => $target->id));
557 $notifications = ($sub->jabber || $sub->sms);
561 $details['notifications_enabled'] = $notifications;
562 $details['blocking'] = $source->hasBlocked($target);
563 $details['id'] = intval($source->id);
568 function showTwitterXmlRelationship($relationship)
570 $this->elementStart('relationship');
572 foreach($relationship as $element => $value) {
573 if ($element == 'source' || $element == 'target') {
574 $this->elementStart($element);
575 $this->showXmlRelationshipDetails($value);
576 $this->elementEnd($element);
580 $this->elementEnd('relationship');
583 function showXmlRelationshipDetails($details)
585 foreach($details as $element => $value) {
586 $this->element($element, null, $value);
590 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
594 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
596 $this->elementStart($tag, $attrs);
597 foreach($twitter_status as $element => $value) {
600 $this->showTwitterXmlUser($twitter_status['user']);
603 $this->element($element, null, common_xml_safe_str($value));
606 $this->showXmlAttachments($twitter_status['attachments']);
609 $this->showGeoXML($value);
611 case 'retweeted_status':
612 $this->showTwitterXmlStatus($value, 'retweeted_status');
615 if (strncmp($element, 'statusnet_', 10) == 0) {
616 $this->element('statusnet:'.substr($element, 10), null, $value);
618 $this->element($element, null, $value);
622 $this->elementEnd($tag);
625 function showTwitterXmlGroup($twitter_group)
627 $this->elementStart('group');
628 foreach($twitter_group as $element => $value) {
629 $this->element($element, null, $value);
631 $this->elementEnd('group');
634 function showTwitterXmlList($twitter_list)
636 $this->elementStart('list');
637 foreach($twitter_list as $element => $value) {
638 if($element == 'user') {
639 $this->showTwitterXmlUser($value, 'user');
642 $this->element($element, null, $value);
645 $this->elementEnd('list');
648 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
652 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
654 $this->elementStart($role, $attrs);
655 foreach($twitter_user as $element => $value) {
656 if ($element == 'status') {
657 $this->showTwitterXmlStatus($twitter_user['status']);
658 } else if (strncmp($element, 'statusnet_', 10) == 0) {
659 $this->element('statusnet:'.substr($element, 10), null, $value);
661 $this->element($element, null, $value);
664 $this->elementEnd($role);
667 function showXmlAttachments($attachments) {
668 if (!empty($attachments)) {
669 $this->elementStart('attachments', array('type' => 'array'));
670 foreach ($attachments as $attachment) {
672 $attrs['url'] = $attachment['url'];
673 $attrs['mimetype'] = $attachment['mimetype'];
674 $attrs['size'] = $attachment['size'];
675 $this->element('enclosure', $attrs, '');
677 $this->elementEnd('attachments');
681 function showGeoXML($geo)
685 $this->element('geo');
687 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
688 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
689 $this->elementEnd('geo');
693 function showGeoRSS($geo)
699 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
704 function showTwitterRssItem($entry)
706 $this->elementStart('item');
707 $this->element('title', null, $entry['title']);
708 $this->element('description', null, $entry['description']);
709 $this->element('pubDate', null, $entry['pubDate']);
710 $this->element('guid', null, $entry['guid']);
711 $this->element('link', null, $entry['link']);
713 // RSS only supports 1 enclosure per item
714 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
715 $enclosure = $entry['enclosures'][0];
716 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
719 if(array_key_exists('tags', $entry)){
720 foreach($entry['tags'] as $tag){
721 $this->element('category', null,$tag);
725 $this->showGeoRSS($entry['geo']);
726 $this->elementEnd('item');
729 function showJsonObjects($objects)
731 print(json_encode($objects));
734 function showSingleXmlStatus($notice)
736 $this->initDocument('xml');
737 $twitter_status = $this->twitterStatusArray($notice);
738 $this->showTwitterXmlStatus($twitter_status, 'status', true);
739 $this->endDocument('xml');
742 function showSingleAtomStatus($notice)
744 header('Content-Type: application/atom+xml; charset=utf-8');
745 print $notice->asAtomEntry(true, true, true, $this->auth_user);
748 function show_single_json_status($notice)
750 $this->initDocument('json');
751 $status = $this->twitterStatusArray($notice);
752 $this->showJsonObjects($status);
753 $this->endDocument('json');
756 function showXmlTimeline($notice)
758 $this->initDocument('xml');
759 $this->elementStart('statuses', array('type' => 'array',
760 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
762 if (is_array($notice)) {
763 $notice = new ArrayWrapper($notice);
766 while ($notice->fetch()) {
768 $twitter_status = $this->twitterStatusArray($notice);
769 $this->showTwitterXmlStatus($twitter_status);
770 } catch (Exception $e) {
771 common_log(LOG_ERR, $e->getMessage());
776 $this->elementEnd('statuses');
777 $this->endDocument('xml');
780 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
782 $this->initDocument('rss');
784 $this->element('title', null, $title);
785 $this->element('link', null, $link);
787 if (!is_null($self)) {
791 'type' => 'application/rss+xml',
798 if (!is_null($suplink)) {
799 // For FriendFeed's SUP protocol
800 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
801 'rel' => 'http://api.friendfeed.com/2008/03#sup',
803 'type' => 'application/json'));
806 if (!is_null($logo)) {
807 $this->elementStart('image');
808 $this->element('link', null, $link);
809 $this->element('title', null, $title);
810 $this->element('url', null, $logo);
811 $this->elementEnd('image');
814 $this->element('description', null, $subtitle);
815 $this->element('language', null, 'en-us');
816 $this->element('ttl', null, '40');
818 if (is_array($notice)) {
819 $notice = new ArrayWrapper($notice);
822 while ($notice->fetch()) {
824 $entry = $this->twitterRssEntryArray($notice);
825 $this->showTwitterRssItem($entry);
826 } catch (Exception $e) {
827 common_log(LOG_ERR, $e->getMessage());
828 // continue on exceptions
832 $this->endTwitterRss();
835 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
837 $this->initDocument('atom');
839 $this->element('title', null, $title);
840 $this->element('id', null, $id);
841 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
843 if (!is_null($logo)) {
844 $this->element('logo',null,$logo);
847 if (!is_null($suplink)) {
848 // For FriendFeed's SUP protocol
849 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
851 'type' => 'application/json'));
854 if (!is_null($selfuri)) {
855 $this->element('link', array('href' => $selfuri,
856 'rel' => 'self', 'type' => 'application/atom+xml'), null);
859 $this->element('updated', null, common_date_iso8601('now'));
860 $this->element('subtitle', null, $subtitle);
862 if (is_array($notice)) {
863 $notice = new ArrayWrapper($notice);
866 while ($notice->fetch()) {
868 $this->raw($notice->asAtomEntry());
869 } catch (Exception $e) {
870 common_log(LOG_ERR, $e->getMessage());
875 $this->endDocument('atom');
878 function showRssGroups($group, $title, $link, $subtitle)
880 $this->initDocument('rss');
882 $this->element('title', null, $title);
883 $this->element('link', null, $link);
884 $this->element('description', null, $subtitle);
885 $this->element('language', null, 'en-us');
886 $this->element('ttl', null, '40');
888 if (is_array($group)) {
889 foreach ($group as $g) {
890 $twitter_group = $this->twitterRssGroupArray($g);
891 $this->showTwitterRssItem($twitter_group);
894 while ($group->fetch()) {
895 $twitter_group = $this->twitterRssGroupArray($group);
896 $this->showTwitterRssItem($twitter_group);
900 $this->endTwitterRss();
903 function showTwitterAtomEntry($entry)
905 $this->elementStart('entry');
906 $this->element('title', null, common_xml_safe_str($entry['title']));
909 array('type' => 'html'),
910 common_xml_safe_str($entry['content'])
912 $this->element('id', null, $entry['id']);
913 $this->element('published', null, $entry['published']);
914 $this->element('updated', null, $entry['updated']);
915 $this->element('link', array('type' => 'text/html',
916 'href' => $entry['link'],
917 'rel' => 'alternate'));
918 $this->element('link', array('type' => $entry['avatar-type'],
919 'href' => $entry['avatar'],
921 $this->elementStart('author');
923 $this->element('name', null, $entry['author-name']);
924 $this->element('uri', null, $entry['author-uri']);
926 $this->elementEnd('author');
927 $this->elementEnd('entry');
930 function showXmlDirectMessage($dm, $namespaces=false)
934 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
936 $this->elementStart('direct_message', $attrs);
937 foreach($dm as $element => $value) {
941 $this->showTwitterXmlUser($value, $element);
944 $this->element($element, null, common_xml_safe_str($value));
947 $this->element($element, null, $value);
951 $this->elementEnd('direct_message');
954 function directMessageArray($message)
958 $from_profile = $message->getFrom();
959 $to_profile = $message->getTo();
961 $dmsg['id'] = intval($message->id);
962 $dmsg['sender_id'] = intval($from_profile);
963 $dmsg['text'] = trim($message->content);
964 $dmsg['recipient_id'] = intval($to_profile);
965 $dmsg['created_at'] = $this->dateTwitter($message->created);
966 $dmsg['sender_screen_name'] = $from_profile->nickname;
967 $dmsg['recipient_screen_name'] = $to_profile->nickname;
968 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
969 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
974 function rssDirectMessageArray($message)
978 $from = $message->getFrom();
980 $entry['title'] = sprintf('Message from %1$s to %2$s',
981 $from->nickname, $message->getTo()->nickname);
983 $entry['content'] = common_xml_safe_str($message->rendered);
984 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
985 $entry['published'] = common_date_iso8601($message->created);
987 $taguribase = TagURI::base();
989 $entry['id'] = "tag:$taguribase:$entry[link]";
990 $entry['updated'] = $entry['published'];
992 $entry['author-name'] = $from->getBestName();
993 $entry['author-uri'] = $from->homepage;
995 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
997 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
998 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
1000 // RSS item specific
1002 $entry['description'] = $entry['content'];
1003 $entry['pubDate'] = common_date_rfc2822($message->created);
1004 $entry['guid'] = $entry['link'];
1009 function showSingleXmlDirectMessage($message)
1011 $this->initDocument('xml');
1012 $dmsg = $this->directMessageArray($message);
1013 $this->showXmlDirectMessage($dmsg, true);
1014 $this->endDocument('xml');
1017 function showSingleJsonDirectMessage($message)
1019 $this->initDocument('json');
1020 $dmsg = $this->directMessageArray($message);
1021 $this->showJsonObjects($dmsg);
1022 $this->endDocument('json');
1025 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1027 $this->initDocument('atom');
1029 $this->element('title', null, common_xml_safe_str($title));
1030 $this->element('id', null, $id);
1031 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1033 if (!is_null($selfuri)) {
1034 $this->element('link', array('href' => $selfuri,
1035 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1038 $this->element('updated', null, common_date_iso8601('now'));
1039 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1041 if (is_array($group)) {
1042 foreach ($group as $g) {
1043 $this->raw($g->asAtomEntry());
1046 while ($group->fetch()) {
1047 $this->raw($group->asAtomEntry());
1051 $this->endDocument('atom');
1055 function showJsonTimeline($notice)
1057 $this->initDocument('json');
1059 $statuses = array();
1061 if (is_array($notice)) {
1062 $notice = new ArrayWrapper($notice);
1065 while ($notice->fetch()) {
1067 $twitter_status = $this->twitterStatusArray($notice);
1068 array_push($statuses, $twitter_status);
1069 } catch (Exception $e) {
1070 common_log(LOG_ERR, $e->getMessage());
1075 $this->showJsonObjects($statuses);
1077 $this->endDocument('json');
1080 function showJsonGroups($group)
1082 $this->initDocument('json');
1086 if (is_array($group)) {
1087 foreach ($group as $g) {
1088 $twitter_group = $this->twitterGroupArray($g);
1089 array_push($groups, $twitter_group);
1092 while ($group->fetch()) {
1093 $twitter_group = $this->twitterGroupArray($group);
1094 array_push($groups, $twitter_group);
1098 $this->showJsonObjects($groups);
1100 $this->endDocument('json');
1103 function showXmlGroups($group)
1106 $this->initDocument('xml');
1107 $this->elementStart('groups', array('type' => 'array'));
1109 if (is_array($group)) {
1110 foreach ($group as $g) {
1111 $twitter_group = $this->twitterGroupArray($g);
1112 $this->showTwitterXmlGroup($twitter_group);
1115 while ($group->fetch()) {
1116 $twitter_group = $this->twitterGroupArray($group);
1117 $this->showTwitterXmlGroup($twitter_group);
1121 $this->elementEnd('groups');
1122 $this->endDocument('xml');
1125 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1128 $this->initDocument('xml');
1129 $this->elementStart('lists_list');
1130 $this->elementStart('lists', array('type' => 'array'));
1132 if (is_array($list)) {
1133 foreach ($list as $l) {
1134 $twitter_list = $this->twitterListArray($l);
1135 $this->showTwitterXmlList($twitter_list);
1138 while ($list->fetch()) {
1139 $twitter_list = $this->twitterListArray($list);
1140 $this->showTwitterXmlList($twitter_list);
1144 $this->elementEnd('lists');
1146 $this->element('next_cursor', null, $next_cursor);
1147 $this->element('previous_cursor', null, $prev_cursor);
1149 $this->elementEnd('lists_list');
1150 $this->endDocument('xml');
1153 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1155 $this->initDocument('json');
1159 if (is_array($list)) {
1160 foreach ($list as $l) {
1161 $twitter_list = $this->twitterListArray($l);
1162 array_push($lists, $twitter_list);
1165 while ($list->fetch()) {
1166 $twitter_list = $this->twitterListArray($list);
1167 array_push($lists, $twitter_list);
1171 $lists_list = array(
1173 'next_cursor' => $next_cursor,
1174 'next_cursor_str' => strval($next_cursor),
1175 'previous_cursor' => $prev_cursor,
1176 'previous_cursor_str' => strval($prev_cursor)
1179 $this->showJsonObjects($lists_list);
1181 $this->endDocument('json');
1184 function showTwitterXmlUsers($user)
1186 $this->initDocument('xml');
1187 $this->elementStart('users', array('type' => 'array',
1188 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1190 if (is_array($user)) {
1191 foreach ($user as $u) {
1192 $twitter_user = $this->twitterUserArray($u);
1193 $this->showTwitterXmlUser($twitter_user);
1196 while ($user->fetch()) {
1197 $twitter_user = $this->twitterUserArray($user);
1198 $this->showTwitterXmlUser($twitter_user);
1202 $this->elementEnd('users');
1203 $this->endDocument('xml');
1206 function showJsonUsers($user)
1208 $this->initDocument('json');
1212 if (is_array($user)) {
1213 foreach ($user as $u) {
1214 $twitter_user = $this->twitterUserArray($u);
1215 array_push($users, $twitter_user);
1218 while ($user->fetch()) {
1219 $twitter_user = $this->twitterUserArray($user);
1220 array_push($users, $twitter_user);
1224 $this->showJsonObjects($users);
1226 $this->endDocument('json');
1229 function showSingleJsonGroup($group)
1231 $this->initDocument('json');
1232 $twitter_group = $this->twitterGroupArray($group);
1233 $this->showJsonObjects($twitter_group);
1234 $this->endDocument('json');
1237 function showSingleXmlGroup($group)
1239 $this->initDocument('xml');
1240 $twitter_group = $this->twitterGroupArray($group);
1241 $this->showTwitterXmlGroup($twitter_group);
1242 $this->endDocument('xml');
1245 function showSingleJsonList($list)
1247 $this->initDocument('json');
1248 $twitter_list = $this->twitterListArray($list);
1249 $this->showJsonObjects($twitter_list);
1250 $this->endDocument('json');
1253 function showSingleXmlList($list)
1255 $this->initDocument('xml');
1256 $twitter_list = $this->twitterListArray($list);
1257 $this->showTwitterXmlList($twitter_list);
1258 $this->endDocument('xml');
1261 function dateTwitter($dt)
1263 $dateStr = date('d F Y H:i:s', strtotime($dt));
1264 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1265 $d->setTimezone(new DateTimeZone(common_timezone()));
1266 return $d->format('D M d H:i:s O Y');
1269 function initDocument($type='xml')
1273 header('Content-Type: application/xml; charset=utf-8');
1277 header('Content-Type: application/json; charset=utf-8');
1279 // Check for JSONP callback
1280 if (isset($this->callback)) {
1281 print $this->callback . '(';
1285 header("Content-Type: application/rss+xml; charset=utf-8");
1286 $this->initTwitterRss();
1289 header('Content-Type: application/atom+xml; charset=utf-8');
1290 $this->initTwitterAtom();
1293 // TRANS: Client error on an API request with an unsupported data format.
1294 $this->clientError(_('Not a supported data format.'));
1301 function endDocument($type='xml')
1308 // Check for JSONP callback
1309 if (isset($this->callback)) {
1314 $this->endTwitterRss();
1317 $this->endTwitterRss();
1320 // TRANS: Client error on an API request with an unsupported data format.
1321 $this->clientError(_('Not a supported data format.'));
1327 function clientError($msg, $code = 400, $format = null)
1329 $action = $this->trimmed('action');
1330 if ($format === null) {
1331 $format = $this->format;
1334 common_debug("User error '$code' on '$action': $msg", __FILE__);
1336 if (!array_key_exists($code, ClientErrorAction::$status)) {
1340 $status_string = ClientErrorAction::$status[$code];
1342 // Do not emit error header for JSONP
1343 if (!isset($this->callback)) {
1344 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1349 $this->initDocument('xml');
1350 $this->elementStart('hash');
1351 $this->element('error', null, $msg);
1352 $this->element('request', null, $_SERVER['REQUEST_URI']);
1353 $this->elementEnd('hash');
1354 $this->endDocument('xml');
1357 $this->initDocument('json');
1358 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1359 print(json_encode($error_array));
1360 $this->endDocument('json');
1363 header('Content-Type: text/plain; charset=utf-8');
1367 // If user didn't request a useful format, throw a regular client error
1368 throw new ClientException($msg, $code);
1372 function serverError($msg, $code = 500, $content_type = null)
1374 $action = $this->trimmed('action');
1375 if ($content_type === null) {
1376 $content_type = $this->format;
1379 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1381 if (!array_key_exists($code, ServerErrorAction::$status)) {
1385 $status_string = ServerErrorAction::$status[$code];
1387 // Do not emit error header for JSONP
1388 if (!isset($this->callback)) {
1389 header('HTTP/1.1 '.$code.' '.$status_string);
1392 if ($content_type == 'xml') {
1393 $this->initDocument('xml');
1394 $this->elementStart('hash');
1395 $this->element('error', null, $msg);
1396 $this->element('request', null, $_SERVER['REQUEST_URI']);
1397 $this->elementEnd('hash');
1398 $this->endDocument('xml');
1400 $this->initDocument('json');
1401 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1402 print(json_encode($error_array));
1403 $this->endDocument('json');
1407 function initTwitterRss()
1410 $this->elementStart(
1414 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1415 'xmlns:georss' => 'http://www.georss.org/georss'
1418 $this->elementStart('channel');
1419 Event::handle('StartApiRss', array($this));
1422 function endTwitterRss()
1424 $this->elementEnd('channel');
1425 $this->elementEnd('rss');
1429 function initTwitterAtom()
1432 // FIXME: don't hardcode the language here!
1433 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1434 'xml:lang' => 'en-US',
1435 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1438 function endTwitterAtom()
1440 $this->elementEnd('feed');
1444 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1446 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1447 switch ($content_type) {
1449 $this->showTwitterXmlUser($profile_array);
1452 $this->showJsonObjects($profile_array);
1455 // TRANS: Client error on an API request with an unsupported data format.
1456 $this->clientError(_('Not a supported data format.'));
1462 private static function is_decimal($str)
1464 return preg_match('/^[0-9]+$/', $str);
1467 function getTargetUser($id)
1470 // Twitter supports these other ways of passing the user ID
1471 if (self::is_decimal($this->arg('id'))) {
1472 return User::staticGet($this->arg('id'));
1473 } else if ($this->arg('id')) {
1474 $nickname = common_canonical_nickname($this->arg('id'));
1475 return User::staticGet('nickname', $nickname);
1476 } else if ($this->arg('user_id')) {
1477 // This is to ensure that a non-numeric user_id still
1478 // overrides screen_name even if it doesn't get used
1479 if (self::is_decimal($this->arg('user_id'))) {
1480 return User::staticGet('id', $this->arg('user_id'));
1482 } else if ($this->arg('screen_name')) {
1483 $nickname = common_canonical_nickname($this->arg('screen_name'));
1484 return User::staticGet('nickname', $nickname);
1486 // Fall back to trying the currently authenticated user
1487 return $this->auth_user;
1490 } else if (self::is_decimal($id)) {
1491 return User::staticGet($id);
1493 $nickname = common_canonical_nickname($id);
1494 return User::staticGet('nickname', $nickname);
1498 function getTargetProfile($id)
1502 // Twitter supports these other ways of passing the user ID
1503 if (self::is_decimal($this->arg('id'))) {
1504 return Profile::staticGet($this->arg('id'));
1505 } else if ($this->arg('id')) {
1506 // Screen names currently can only uniquely identify a local user.
1507 $nickname = common_canonical_nickname($this->arg('id'));
1508 $user = User::staticGet('nickname', $nickname);
1509 return $user ? $user->getProfile() : null;
1510 } else if ($this->arg('user_id')) {
1511 // This is to ensure that a non-numeric user_id still
1512 // overrides screen_name even if it doesn't get used
1513 if (self::is_decimal($this->arg('user_id'))) {
1514 return Profile::staticGet('id', $this->arg('user_id'));
1516 } else if ($this->arg('screen_name')) {
1517 $nickname = common_canonical_nickname($this->arg('screen_name'));
1518 $user = User::staticGet('nickname', $nickname);
1519 return $user ? $user->getProfile() : null;
1521 } else if (self::is_decimal($id)) {
1522 return Profile::staticGet($id);
1524 $nickname = common_canonical_nickname($id);
1525 $user = User::staticGet('nickname', $nickname);
1526 return $user ? $user->getProfile() : null;
1530 function getTargetGroup($id)
1533 if (self::is_decimal($this->arg('id'))) {
1534 return User_group::staticGet('id', $this->arg('id'));
1535 } else if ($this->arg('id')) {
1536 return User_group::getForNickname($this->arg('id'));
1537 } else if ($this->arg('group_id')) {
1538 // This is to ensure that a non-numeric group_id still
1539 // overrides group_name even if it doesn't get used
1540 if (self::is_decimal($this->arg('group_id'))) {
1541 return User_group::staticGet('id', $this->arg('group_id'));
1543 } else if ($this->arg('group_name')) {
1544 return User_group::getForNickname($this->arg('group_name'));
1547 } else if (self::is_decimal($id)) {
1548 return User_group::staticGet('id', $id);
1550 return User_group::getForNickname($id);
1554 function getTargetList($user=null, $id=null)
1556 $tagger = $this->getTargetUser($user);
1560 $id = $this->arg('id');
1564 if (is_numeric($id)) {
1565 $list = Profile_list::staticGet('id', $id);
1567 // only if the list with the id belongs to the tagger
1568 if(empty($list) || $list->tagger != $tagger->id) {
1573 $tag = common_canonical_tag($id);
1574 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1577 if (!empty($list) && $list->private) {
1578 if ($this->auth_user->id == $list->tagger) {
1589 * Returns query argument or default value if not found. Certain
1590 * parameters used throughout the API are lightly scrubbed and
1591 * bounds checked. This overrides Action::arg().
1593 * @param string $key requested argument
1594 * @param string $def default value to return if $key is not provided
1598 function arg($key, $def=null)
1600 // XXX: Do even more input validation/scrubbing?
1602 if (array_key_exists($key, $this->args)) {
1605 $page = (int)$this->args['page'];
1606 return ($page < 1) ? 1 : $page;
1608 $count = (int)$this->args['count'];
1611 } elseif ($count > 200) {
1617 $since_id = (int)$this->args['since_id'];
1618 return ($since_id < 1) ? 0 : $since_id;
1620 $max_id = (int)$this->args['max_id'];
1621 return ($max_id < 1) ? 0 : $max_id;
1623 return parent::arg($key, $def);
1631 * Calculate the complete URI that called up this action. Used for
1632 * Atom rel="self" links. Warning: this is funky.
1634 * @return string URL a URL suitable for rel="self" Atom links
1636 function getSelfUri()
1638 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1640 $id = $this->arg('id');
1641 $aargs = array('format' => $this->format);
1646 $tag = $this->arg('tag');
1648 $aargs['tag'] = $tag;
1651 parse_str($_SERVER['QUERY_STRING'], $params);
1653 if (!empty($params)) {
1654 unset($params['p']);
1655 $pstring = http_build_query($params);
1658 $uri = common_local_url($action, $aargs);
1660 if (!empty($pstring)) {
1661 $uri .= '?' . $pstring;