3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009 StatusNet, Inc.
31 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
32 * @link http://status.net/
35 /* External API usage documentation. Please update when you change how the API works. */
37 /*! @mainpage StatusNet REST API
41 Some explanatory text about the API would be nice.
45 @subsection timelinesmethods_sec Timeline Methods
47 @li @ref publictimeline
48 @li @ref friendstimeline
50 @subsection statusmethods_sec Status Methods
52 @li @ref statusesupdate
54 @subsection usermethods_sec User Methods
56 @subsection directmessagemethods_sec Direct Message Methods
58 @subsection friendshipmethods_sec Friendship Methods
60 @subsection socialgraphmethods_sec Social Graph Methods
62 @subsection accountmethods_sec Account Methods
64 @subsection favoritesmethods_sec Favorites Methods
66 @subsection blockmethods_sec Block Methods
68 @subsection oauthmethods_sec OAuth Methods
70 @subsection helpmethods_sec Help Methods
72 @subsection groupmethods_sec Group Methods
74 @page apiroot API Root
76 The URLs for methods referred to in this API documentation are
77 relative to the StatusNet API root. The API root is determined by the
78 site's @b server and @b path variables, which are generally specified
79 in config.php. For example:
82 $config['site']['server'] = 'example.org';
83 $config['site']['path'] = 'statusnet'
86 The pattern for a site's API root is: @c protocol://server/path/api E.g:
88 @c http://example.org/statusnet/api
90 The @b path can be empty. In that case the API root would simply be:
92 @c http://example.org/api
96 if (!defined('STATUSNET')) {
101 * Contains most of the Twitter-compatible API output functions.
105 * @author Craig Andrews <candrews@integralblue.com>
106 * @author Dan Moore <dan@moore.cx>
107 * @author Evan Prodromou <evan@status.net>
108 * @author Jeffery To <jeffery.to@gmail.com>
109 * @author Toby Inkster <mail@tobyinkster.co.uk>
110 * @author Zach Copley <zach@status.net>
111 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
112 * @link http://status.net/
115 class ApiAction extends Action
118 const READ_WRITE = 2;
122 var $auth_user = null;
126 var $since_id = null;
129 var $access = self::READ_ONLY; // read (default) or read-write
131 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
136 * @param array $args Web and URL arguments
138 * @return boolean false if user doesn't exist
141 function prepare($args)
143 StatusNet::setApi(true); // reduce exception reports to aid in debugging
144 parent::prepare($args);
146 $this->format = $this->arg('format');
147 $this->page = (int)$this->arg('page', 1);
148 $this->count = (int)$this->arg('count', 20);
149 $this->max_id = (int)$this->arg('max_id', 0);
150 $this->since_id = (int)$this->arg('since_id', 0);
152 if ($this->arg('since')) {
153 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
156 $this->source = $this->trimmed('source');
158 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
159 $this->source = 'api';
168 * @param array $args Arguments from $_REQUEST
173 function handle($args)
175 header('Access-Control-Allow-Origin: *');
176 parent::handle($args);
180 * Overrides XMLOutputter::element to write booleans as strings (true|false).
181 * See that method's documentation for more info.
183 * @param string $tag Element type or tagname
184 * @param array $attrs Array of element attributes, as
186 * @param string $content string content of the element
190 function element($tag, $attrs=null, $content=null)
192 if (is_bool($content)) {
193 $content = ($content ? 'true' : 'false');
196 return parent::element($tag, $attrs, $content);
199 function twitterUserArray($profile, $get_notice=false)
201 $twitter_user = array();
203 $twitter_user['id'] = intval($profile->id);
204 $twitter_user['name'] = $profile->getBestName();
205 $twitter_user['screen_name'] = $profile->nickname;
206 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
207 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
209 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
210 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
211 Avatar::defaultImage(AVATAR_STREAM_SIZE);
213 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
214 $twitter_user['protected'] = false; # not supported by StatusNet yet
215 $twitter_user['followers_count'] = $profile->subscriberCount();
218 $user = $profile->getUser();
220 // Note: some profiles don't have an associated user
222 $defaultDesign = Design::siteDesign();
225 $design = $user->getDesign();
228 if (empty($design)) {
229 $design = $defaultDesign;
232 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
233 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
234 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
235 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
236 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
237 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
238 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
239 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
240 $twitter_user['profile_sidebar_border_color'] = '';
242 $twitter_user['friends_count'] = $profile->subscriptionCount();
244 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
246 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
250 if (!empty($user) && $user->timezone) {
251 $timezone = $user->timezone;
255 $t->setTimezone(new DateTimeZone($timezone));
257 $twitter_user['utc_offset'] = $t->format('Z');
258 $twitter_user['time_zone'] = $timezone;
260 $twitter_user['profile_background_image_url']
261 = empty($design->backgroundimage)
262 ? '' : ($design->disposition & BACKGROUND_ON)
263 ? Design::url($design->backgroundimage) : '';
265 $twitter_user['profile_background_tile']
266 = empty($design->disposition)
267 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
269 $twitter_user['statuses_count'] = $profile->noticeCount();
271 // Is the requesting user following this user?
272 $twitter_user['following'] = false;
273 $twitter_user['notifications'] = false;
275 if (isset($this->auth_user)) {
277 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
280 $sub = Subscription::pkeyGet(array('subscriber' =>
281 $this->auth_user->id,
282 'subscribed' => $profile->id));
285 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
290 $notice = $profile->getCurrentNotice();
293 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
297 return $twitter_user;
300 function twitterStatusArray($notice, $include_user=true)
302 $base = $this->twitterSimpleStatusArray($notice, $include_user);
304 if (!empty($notice->repeat_of)) {
305 $original = Notice::staticGet('id', $notice->repeat_of);
306 if (!empty($original)) {
307 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
308 $base['retweeted_status'] = $original_array;
315 function twitterSimpleStatusArray($notice, $include_user=true)
317 $profile = $notice->getProfile();
319 $twitter_status = array();
320 $twitter_status['text'] = $notice->content;
321 $twitter_status['truncated'] = false; # Not possible on StatusNet
322 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
323 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
324 intval($notice->reply_to) : null;
328 $ns = $notice->getSource();
330 if (!empty($ns->name) && !empty($ns->url)) {
331 $source = '<a href="'
332 . htmlspecialchars($ns->url)
333 . '" rel="nofollow">'
334 . htmlspecialchars($ns->name)
341 $twitter_status['source'] = $source;
342 $twitter_status['id'] = intval($notice->id);
344 $replier_profile = null;
346 if ($notice->reply_to) {
347 $reply = Notice::staticGet(intval($notice->reply_to));
349 $replier_profile = $reply->getProfile();
353 $twitter_status['in_reply_to_user_id'] =
354 ($replier_profile) ? intval($replier_profile->id) : null;
355 $twitter_status['in_reply_to_screen_name'] =
356 ($replier_profile) ? $replier_profile->nickname : null;
358 if (isset($notice->lat) && isset($notice->lon)) {
359 // This is the format that GeoJSON expects stuff to be in
360 $twitter_status['geo'] = array('type' => 'Point',
361 'coordinates' => array((float) $notice->lat,
362 (float) $notice->lon));
364 $twitter_status['geo'] = null;
367 if (isset($this->auth_user)) {
368 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
370 $twitter_status['favorited'] = false;
374 $attachments = $notice->attachments();
376 if (!empty($attachments)) {
378 $twitter_status['attachments'] = array();
380 foreach ($attachments as $attachment) {
381 $enclosure_o=$attachment->getEnclosure();
383 $enclosure = array();
384 $enclosure['url'] = $enclosure_o->url;
385 $enclosure['mimetype'] = $enclosure_o->mimetype;
386 $enclosure['size'] = $enclosure_o->size;
387 $twitter_status['attachments'][] = $enclosure;
392 if ($include_user && $profile) {
393 # Don't get notice (recursive!)
394 $twitter_user = $this->twitterUserArray($profile, false);
395 $twitter_status['user'] = $twitter_user;
398 return $twitter_status;
401 function twitterGroupArray($group)
403 $twitter_group=array();
404 $twitter_group['id']=$group->id;
405 $twitter_group['url']=$group->permalink();
406 $twitter_group['nickname']=$group->nickname;
407 $twitter_group['fullname']=$group->fullname;
408 $twitter_group['original_logo']=$group->original_logo;
409 $twitter_group['homepage_logo']=$group->homepage_logo;
410 $twitter_group['stream_logo']=$group->stream_logo;
411 $twitter_group['mini_logo']=$group->mini_logo;
412 $twitter_group['homepage']=$group->homepage;
413 $twitter_group['description']=$group->description;
414 $twitter_group['location']=$group->location;
415 $twitter_group['created']=$this->dateTwitter($group->created);
416 $twitter_group['modified']=$this->dateTwitter($group->modified);
417 return $twitter_group;
420 function twitterRssGroupArray($group)
423 $entry['content']=$group->description;
424 $entry['title']=$group->nickname;
425 $entry['link']=$group->permalink();
426 $entry['published']=common_date_iso8601($group->created);
427 $entry['updated']==common_date_iso8601($group->modified);
428 $taguribase = common_config('integration', 'groupuri');
429 $entry['id'] = "group:$groupuribase:$entry[link]";
431 $entry['description'] = $entry['content'];
432 $entry['pubDate'] = common_date_rfc2822($group->created);
433 $entry['guid'] = $entry['link'];
438 function twitterRssEntryArray($notice)
440 $profile = $notice->getProfile();
443 // We trim() to avoid extraneous whitespace in the output
445 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
446 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
447 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
448 $entry['published'] = common_date_iso8601($notice->created);
450 $taguribase = TagURI::base();
451 $entry['id'] = "tag:$taguribase:$entry[link]";
453 $entry['updated'] = $entry['published'];
454 $entry['author'] = $profile->getBestName();
457 $attachments = $notice->attachments();
458 $enclosures = array();
460 foreach ($attachments as $attachment) {
461 $enclosure_o=$attachment->getEnclosure();
463 $enclosure = array();
464 $enclosure['url'] = $enclosure_o->url;
465 $enclosure['mimetype'] = $enclosure_o->mimetype;
466 $enclosure['size'] = $enclosure_o->size;
467 $enclosures[] = $enclosure;
471 if (!empty($enclosures)) {
472 $entry['enclosures'] = $enclosures;
476 $tag = new Notice_tag();
477 $tag->notice_id = $notice->id;
479 $entry['tags']=array();
480 while ($tag->fetch()) {
481 $entry['tags'][]=$tag->tag;
487 $entry['description'] = $entry['content'];
488 $entry['pubDate'] = common_date_rfc2822($notice->created);
489 $entry['guid'] = $entry['link'];
491 if (isset($notice->lat) && isset($notice->lon)) {
492 // This is the format that GeoJSON expects stuff to be in.
493 // showGeoRSS() below uses it for XML output, so we reuse it
494 $entry['geo'] = array('type' => 'Point',
495 'coordinates' => array((float) $notice->lat,
496 (float) $notice->lon));
498 $entry['geo'] = null;
504 function twitterRelationshipArray($source, $target)
506 $relationship = array();
508 $relationship['source'] =
509 $this->relationshipDetailsArray($source, $target);
510 $relationship['target'] =
511 $this->relationshipDetailsArray($target, $source);
513 return array('relationship' => $relationship);
516 function relationshipDetailsArray($source, $target)
520 $details['screen_name'] = $source->nickname;
521 $details['followed_by'] = $target->isSubscribed($source);
522 $details['following'] = $source->isSubscribed($target);
524 $notifications = false;
526 if ($source->isSubscribed($target)) {
528 $sub = Subscription::pkeyGet(array('subscriber' =>
529 $source->id, 'subscribed' => $target->id));
532 $notifications = ($sub->jabber || $sub->sms);
536 $details['notifications_enabled'] = $notifications;
537 $details['blocking'] = $source->hasBlocked($target);
538 $details['id'] = $source->id;
543 function showTwitterXmlRelationship($relationship)
545 $this->elementStart('relationship');
547 foreach($relationship as $element => $value) {
548 if ($element == 'source' || $element == 'target') {
549 $this->elementStart($element);
550 $this->showXmlRelationshipDetails($value);
551 $this->elementEnd($element);
555 $this->elementEnd('relationship');
558 function showXmlRelationshipDetails($details)
560 foreach($details as $element => $value) {
561 $this->element($element, null, $value);
565 function showTwitterXmlStatus($twitter_status, $tag='status')
567 $this->elementStart($tag);
568 foreach($twitter_status as $element => $value) {
571 $this->showTwitterXmlUser($twitter_status['user']);
574 $this->element($element, null, common_xml_safe_str($value));
577 $this->showXmlAttachments($twitter_status['attachments']);
580 $this->showGeoXML($value);
582 case 'retweeted_status':
583 $this->showTwitterXmlStatus($value, 'retweeted_status');
586 $this->element($element, null, $value);
589 $this->elementEnd($tag);
592 function showTwitterXmlGroup($twitter_group)
594 $this->elementStart('group');
595 foreach($twitter_group as $element => $value) {
596 $this->element($element, null, $value);
598 $this->elementEnd('group');
601 function showTwitterXmlUser($twitter_user, $role='user')
603 $this->elementStart($role);
604 foreach($twitter_user as $element => $value) {
605 if ($element == 'status') {
606 $this->showTwitterXmlStatus($twitter_user['status']);
608 $this->element($element, null, $value);
611 $this->elementEnd($role);
614 function showXmlAttachments($attachments) {
615 if (!empty($attachments)) {
616 $this->elementStart('attachments', array('type' => 'array'));
617 foreach ($attachments as $attachment) {
619 $attrs['url'] = $attachment['url'];
620 $attrs['mimetype'] = $attachment['mimetype'];
621 $attrs['size'] = $attachment['size'];
622 $this->element('enclosure', $attrs, '');
624 $this->elementEnd('attachments');
628 function showGeoXML($geo)
632 $this->element('geo');
634 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
635 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
636 $this->elementEnd('geo');
640 function showGeoRSS($geo)
646 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
651 function showTwitterRssItem($entry)
653 $this->elementStart('item');
654 $this->element('title', null, $entry['title']);
655 $this->element('description', null, $entry['description']);
656 $this->element('pubDate', null, $entry['pubDate']);
657 $this->element('guid', null, $entry['guid']);
658 $this->element('link', null, $entry['link']);
660 # RSS only supports 1 enclosure per item
661 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
662 $enclosure = $entry['enclosures'][0];
663 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
666 if(array_key_exists('tags', $entry)){
667 foreach($entry['tags'] as $tag){
668 $this->element('category', null,$tag);
672 $this->showGeoRSS($entry['geo']);
673 $this->elementEnd('item');
676 function showJsonObjects($objects)
678 print(json_encode($objects));
681 function showSingleXmlStatus($notice)
683 $this->initDocument('xml');
684 $twitter_status = $this->twitterStatusArray($notice);
685 $this->showTwitterXmlStatus($twitter_status);
686 $this->endDocument('xml');
689 function show_single_json_status($notice)
691 $this->initDocument('json');
692 $status = $this->twitterStatusArray($notice);
693 $this->showJsonObjects($status);
694 $this->endDocument('json');
697 function showXmlTimeline($notice)
700 $this->initDocument('xml');
701 $this->elementStart('statuses', array('type' => 'array'));
703 if (is_array($notice)) {
704 foreach ($notice as $n) {
705 $twitter_status = $this->twitterStatusArray($n);
706 $this->showTwitterXmlStatus($twitter_status);
709 while ($notice->fetch()) {
710 $twitter_status = $this->twitterStatusArray($notice);
711 $this->showTwitterXmlStatus($twitter_status);
715 $this->elementEnd('statuses');
716 $this->endDocument('xml');
719 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
722 $this->initDocument('rss');
724 $this->element('title', null, $title);
725 $this->element('link', null, $link);
727 if (!is_null($self)) {
731 'type' => 'application/rss+xml',
738 if (!is_null($suplink)) {
739 // For FriendFeed's SUP protocol
740 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
741 'rel' => 'http://api.friendfeed.com/2008/03#sup',
743 'type' => 'application/json'));
746 if (!is_null($logo)) {
747 $this->elementStart('image');
748 $this->element('link', null, $link);
749 $this->element('title', null, $title);
750 $this->element('url', null, $logo);
751 $this->elementEnd('image');
754 $this->element('description', null, $subtitle);
755 $this->element('language', null, 'en-us');
756 $this->element('ttl', null, '40');
758 if (is_array($notice)) {
759 foreach ($notice as $n) {
760 $entry = $this->twitterRssEntryArray($n);
761 $this->showTwitterRssItem($entry);
764 while ($notice->fetch()) {
765 $entry = $this->twitterRssEntryArray($notice);
766 $this->showTwitterRssItem($entry);
770 $this->endTwitterRss();
773 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
776 $this->initDocument('atom');
778 $this->element('title', null, $title);
779 $this->element('id', null, $id);
780 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
782 if (!is_null($logo)) {
783 $this->element('logo',null,$logo);
786 if (!is_null($suplink)) {
787 # For FriendFeed's SUP protocol
788 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
790 'type' => 'application/json'));
793 if (!is_null($selfuri)) {
794 $this->element('link', array('href' => $selfuri,
795 'rel' => 'self', 'type' => 'application/atom+xml'), null);
798 $this->element('updated', null, common_date_iso8601('now'));
799 $this->element('subtitle', null, $subtitle);
801 if (is_array($notice)) {
802 foreach ($notice as $n) {
803 $this->raw($n->asAtomEntry());
806 while ($notice->fetch()) {
807 $this->raw($notice->asAtomEntry());
811 $this->endDocument('atom');
815 function showRssGroups($group, $title, $link, $subtitle)
818 $this->initDocument('rss');
820 $this->element('title', null, $title);
821 $this->element('link', null, $link);
822 $this->element('description', null, $subtitle);
823 $this->element('language', null, 'en-us');
824 $this->element('ttl', null, '40');
826 if (is_array($group)) {
827 foreach ($group as $g) {
828 $twitter_group = $this->twitterRssGroupArray($g);
829 $this->showTwitterRssItem($twitter_group);
832 while ($group->fetch()) {
833 $twitter_group = $this->twitterRssGroupArray($group);
834 $this->showTwitterRssItem($twitter_group);
838 $this->endTwitterRss();
841 function showTwitterAtomEntry($entry)
843 $this->elementStart('entry');
844 $this->element('title', null, common_xml_safe_str($entry['title']));
847 array('type' => 'html'),
848 common_xml_safe_str($entry['content'])
850 $this->element('id', null, $entry['id']);
851 $this->element('published', null, $entry['published']);
852 $this->element('updated', null, $entry['updated']);
853 $this->element('link', array('type' => 'text/html',
854 'href' => $entry['link'],
855 'rel' => 'alternate'));
856 $this->element('link', array('type' => $entry['avatar-type'],
857 'href' => $entry['avatar'],
859 $this->elementStart('author');
861 $this->element('name', null, $entry['author-name']);
862 $this->element('uri', null, $entry['author-uri']);
864 $this->elementEnd('author');
865 $this->elementEnd('entry');
868 function showXmlDirectMessage($dm)
870 $this->elementStart('direct_message');
871 foreach($dm as $element => $value) {
875 $this->showTwitterXmlUser($value, $element);
878 $this->element($element, null, common_xml_safe_str($value));
881 $this->element($element, null, $value);
885 $this->elementEnd('direct_message');
888 function directMessageArray($message)
892 $from_profile = $message->getFrom();
893 $to_profile = $message->getTo();
895 $dmsg['id'] = $message->id;
896 $dmsg['sender_id'] = $message->from_profile;
897 $dmsg['text'] = trim($message->content);
898 $dmsg['recipient_id'] = $message->to_profile;
899 $dmsg['created_at'] = $this->dateTwitter($message->created);
900 $dmsg['sender_screen_name'] = $from_profile->nickname;
901 $dmsg['recipient_screen_name'] = $to_profile->nickname;
902 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
903 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
908 function rssDirectMessageArray($message)
912 $from = $message->getFrom();
914 $entry['title'] = sprintf('Message from %1$s to %2$s',
915 $from->nickname, $message->getTo()->nickname);
917 $entry['content'] = common_xml_safe_str($message->rendered);
918 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
919 $entry['published'] = common_date_iso8601($message->created);
921 $taguribase = TagURI::base();
923 $entry['id'] = "tag:$taguribase:$entry[link]";
924 $entry['updated'] = $entry['published'];
926 $entry['author-name'] = $from->getBestName();
927 $entry['author-uri'] = $from->homepage;
929 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
931 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
932 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
936 $entry['description'] = $entry['content'];
937 $entry['pubDate'] = common_date_rfc2822($message->created);
938 $entry['guid'] = $entry['link'];
943 function showSingleXmlDirectMessage($message)
945 $this->initDocument('xml');
946 $dmsg = $this->directMessageArray($message);
947 $this->showXmlDirectMessage($dmsg);
948 $this->endDocument('xml');
951 function showSingleJsonDirectMessage($message)
953 $this->initDocument('json');
954 $dmsg = $this->directMessageArray($message);
955 $this->showJsonObjects($dmsg);
956 $this->endDocument('json');
959 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
962 $this->initDocument('atom');
964 $this->element('title', null, common_xml_safe_str($title));
965 $this->element('id', null, $id);
966 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
968 if (!is_null($selfuri)) {
969 $this->element('link', array('href' => $selfuri,
970 'rel' => 'self', 'type' => 'application/atom+xml'), null);
973 $this->element('updated', null, common_date_iso8601('now'));
974 $this->element('subtitle', null, common_xml_safe_str($subtitle));
976 if (is_array($group)) {
977 foreach ($group as $g) {
978 $this->raw($g->asAtomEntry());
981 while ($group->fetch()) {
982 $this->raw($group->asAtomEntry());
986 $this->endDocument('atom');
990 function showJsonTimeline($notice)
993 $this->initDocument('json');
997 if (is_array($notice)) {
998 foreach ($notice as $n) {
999 $twitter_status = $this->twitterStatusArray($n);
1000 array_push($statuses, $twitter_status);
1003 while ($notice->fetch()) {
1004 $twitter_status = $this->twitterStatusArray($notice);
1005 array_push($statuses, $twitter_status);
1009 $this->showJsonObjects($statuses);
1011 $this->endDocument('json');
1014 function showJsonGroups($group)
1017 $this->initDocument('json');
1021 if (is_array($group)) {
1022 foreach ($group as $g) {
1023 $twitter_group = $this->twitterGroupArray($g);
1024 array_push($groups, $twitter_group);
1027 while ($group->fetch()) {
1028 $twitter_group = $this->twitterGroupArray($group);
1029 array_push($groups, $twitter_group);
1033 $this->showJsonObjects($groups);
1035 $this->endDocument('json');
1038 function showXmlGroups($group)
1041 $this->initDocument('xml');
1042 $this->elementStart('groups', array('type' => 'array'));
1044 if (is_array($group)) {
1045 foreach ($group as $g) {
1046 $twitter_group = $this->twitterGroupArray($g);
1047 $this->showTwitterXmlGroup($twitter_group);
1050 while ($group->fetch()) {
1051 $twitter_group = $this->twitterGroupArray($group);
1052 $this->showTwitterXmlGroup($twitter_group);
1056 $this->elementEnd('groups');
1057 $this->endDocument('xml');
1060 function showTwitterXmlUsers($user)
1063 $this->initDocument('xml');
1064 $this->elementStart('users', array('type' => 'array'));
1066 if (is_array($user)) {
1067 foreach ($user as $u) {
1068 $twitter_user = $this->twitterUserArray($u);
1069 $this->showTwitterXmlUser($twitter_user);
1072 while ($user->fetch()) {
1073 $twitter_user = $this->twitterUserArray($user);
1074 $this->showTwitterXmlUser($twitter_user);
1078 $this->elementEnd('users');
1079 $this->endDocument('xml');
1082 function showJsonUsers($user)
1085 $this->initDocument('json');
1089 if (is_array($user)) {
1090 foreach ($user as $u) {
1091 $twitter_user = $this->twitterUserArray($u);
1092 array_push($users, $twitter_user);
1095 while ($user->fetch()) {
1096 $twitter_user = $this->twitterUserArray($user);
1097 array_push($users, $twitter_user);
1101 $this->showJsonObjects($users);
1103 $this->endDocument('json');
1106 function showSingleJsonGroup($group)
1108 $this->initDocument('json');
1109 $twitter_group = $this->twitterGroupArray($group);
1110 $this->showJsonObjects($twitter_group);
1111 $this->endDocument('json');
1114 function showSingleXmlGroup($group)
1116 $this->initDocument('xml');
1117 $twitter_group = $this->twitterGroupArray($group);
1118 $this->showTwitterXmlGroup($twitter_group);
1119 $this->endDocument('xml');
1122 function dateTwitter($dt)
1124 $dateStr = date('d F Y H:i:s', strtotime($dt));
1125 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1126 $d->setTimezone(new DateTimeZone(common_timezone()));
1127 return $d->format('D M d H:i:s O Y');
1130 function initDocument($type='xml')
1134 header('Content-Type: application/xml; charset=utf-8');
1138 header('Content-Type: application/json; charset=utf-8');
1140 // Check for JSONP callback
1141 $callback = $this->arg('callback');
1143 print $callback . '(';
1147 header("Content-Type: application/rss+xml; charset=utf-8");
1148 $this->initTwitterRss();
1151 header('Content-Type: application/atom+xml; charset=utf-8');
1152 $this->initTwitterAtom();
1155 // TRANS: Client error on an API request with an unsupported data format.
1156 $this->clientError(_('Not a supported data format.'));
1163 function endDocument($type='xml')
1171 // Check for JSONP callback
1172 $callback = $this->arg('callback');
1178 $this->endTwitterRss();
1181 $this->endTwitterRss();
1184 // TRANS: Client error on an API request with an unsupported data format.
1185 $this->clientError(_('Not a supported data format.'));
1191 function clientError($msg, $code = 400, $format = 'xml')
1193 $action = $this->trimmed('action');
1195 common_debug("User error '$code' on '$action': $msg", __FILE__);
1197 if (!array_key_exists($code, ClientErrorAction::$status)) {
1201 $status_string = ClientErrorAction::$status[$code];
1203 header('HTTP/1.1 '.$code.' '.$status_string);
1205 if ($format == 'xml') {
1206 $this->initDocument('xml');
1207 $this->elementStart('hash');
1208 $this->element('error', null, $msg);
1209 $this->element('request', null, $_SERVER['REQUEST_URI']);
1210 $this->elementEnd('hash');
1211 $this->endDocument('xml');
1212 } elseif ($format == 'json'){
1213 $this->initDocument('json');
1214 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1215 print(json_encode($error_array));
1216 $this->endDocument('json');
1219 // If user didn't request a useful format, throw a regular client error
1220 throw new ClientException($msg, $code);
1224 function serverError($msg, $code = 500, $content_type = 'xml')
1226 $action = $this->trimmed('action');
1228 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1230 if (!array_key_exists($code, ServerErrorAction::$status)) {
1234 $status_string = ServerErrorAction::$status[$code];
1236 header('HTTP/1.1 '.$code.' '.$status_string);
1238 if ($content_type == 'xml') {
1239 $this->initDocument('xml');
1240 $this->elementStart('hash');
1241 $this->element('error', null, $msg);
1242 $this->element('request', null, $_SERVER['REQUEST_URI']);
1243 $this->elementEnd('hash');
1244 $this->endDocument('xml');
1246 $this->initDocument('json');
1247 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1248 print(json_encode($error_array));
1249 $this->endDocument('json');
1253 function initTwitterRss()
1256 $this->elementStart(
1260 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1261 'xmlns:georss' => 'http://www.georss.org/georss'
1264 $this->elementStart('channel');
1265 Event::handle('StartApiRss', array($this));
1268 function endTwitterRss()
1270 $this->elementEnd('channel');
1271 $this->elementEnd('rss');
1275 function initTwitterAtom()
1278 // FIXME: don't hardcode the language here!
1279 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1280 'xml:lang' => 'en-US',
1281 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1284 function endTwitterAtom()
1286 $this->elementEnd('feed');
1290 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1292 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1293 switch ($content_type) {
1295 $this->showTwitterXmlUser($profile_array);
1298 $this->showJsonObjects($profile_array);
1301 // TRANS: Client error on an API request with an unsupported data format.
1302 $this->clientError(_('Not a supported data format.'));
1308 function getTargetUser($id)
1312 // Twitter supports these other ways of passing the user ID
1313 if (is_numeric($this->arg('id'))) {
1314 return User::staticGet($this->arg('id'));
1315 } else if ($this->arg('id')) {
1316 $nickname = common_canonical_nickname($this->arg('id'));
1317 return User::staticGet('nickname', $nickname);
1318 } else if ($this->arg('user_id')) {
1319 // This is to ensure that a non-numeric user_id still
1320 // overrides screen_name even if it doesn't get used
1321 if (is_numeric($this->arg('user_id'))) {
1322 return User::staticGet('id', $this->arg('user_id'));
1324 } else if ($this->arg('screen_name')) {
1325 $nickname = common_canonical_nickname($this->arg('screen_name'));
1326 return User::staticGet('nickname', $nickname);
1328 // Fall back to trying the currently authenticated user
1329 return $this->auth_user;
1332 } else if (is_numeric($id)) {
1333 return User::staticGet($id);
1335 $nickname = common_canonical_nickname($id);
1336 return User::staticGet('nickname', $nickname);
1340 function getTargetGroup($id)
1343 if (is_numeric($this->arg('id'))) {
1344 return User_group::staticGet($this->arg('id'));
1345 } else if ($this->arg('id')) {
1346 $nickname = common_canonical_nickname($this->arg('id'));
1347 $local = Local_group::staticGet('nickname', $nickname);
1348 if (empty($local)) {
1351 return User_group::staticGet('id', $local->id);
1353 } else if ($this->arg('group_id')) {
1354 // This is to ensure that a non-numeric user_id still
1355 // overrides screen_name even if it doesn't get used
1356 if (is_numeric($this->arg('group_id'))) {
1357 return User_group::staticGet('id', $this->arg('group_id'));
1359 } else if ($this->arg('group_name')) {
1360 $nickname = common_canonical_nickname($this->arg('group_name'));
1361 $local = Local_group::staticGet('nickname', $nickname);
1362 if (empty($local)) {
1365 return User_group::staticGet('id', $local->group_id);
1369 } else if (is_numeric($id)) {
1370 return User_group::staticGet($id);
1372 $nickname = common_canonical_nickname($id);
1373 $local = Local_group::staticGet('nickname', $nickname);
1374 if (empty($local)) {
1377 return User_group::staticGet('id', $local->group_id);
1383 * Returns query argument or default value if not found. Certain
1384 * parameters used throughout the API are lightly scrubbed and
1385 * bounds checked. This overrides Action::arg().
1387 * @param string $key requested argument
1388 * @param string $def default value to return if $key is not provided
1392 function arg($key, $def=null)
1395 // XXX: Do even more input validation/scrubbing?
1397 if (array_key_exists($key, $this->args)) {
1400 $page = (int)$this->args['page'];
1401 return ($page < 1) ? 1 : $page;
1403 $count = (int)$this->args['count'];
1406 } elseif ($count > 200) {
1412 $since_id = (int)$this->args['since_id'];
1413 return ($since_id < 1) ? 0 : $since_id;
1415 $max_id = (int)$this->args['max_id'];
1416 return ($max_id < 1) ? 0 : $max_id;
1418 return parent::arg($key, $def);
1426 * Calculate the complete URI that called up this action. Used for
1427 * Atom rel="self" links. Warning: this is funky.
1429 * @return string URL a URL suitable for rel="self" Atom links
1431 function getSelfUri()
1433 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1435 $id = $this->arg('id');
1436 $aargs = array('format' => $this->format);
1441 $tag = $this->arg('tag');
1443 $aargs['tag'] = $tag;
1446 parse_str($_SERVER['QUERY_STRING'], $params);
1448 if (!empty($params)) {
1449 unset($params['p']);
1450 $pstring = http_build_query($params);
1453 $uri = common_local_url($action, $aargs);
1455 if (!empty($pstring)) {
1456 $uri .= '?' . $pstring;