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')) {
100 class ApiValidationException extends Exception { }
103 * Contains most of the Twitter-compatible API output functions.
107 * @author Craig Andrews <candrews@integralblue.com>
108 * @author Dan Moore <dan@moore.cx>
109 * @author Evan Prodromou <evan@status.net>
110 * @author Jeffery To <jeffery.to@gmail.com>
111 * @author Toby Inkster <mail@tobyinkster.co.uk>
112 * @author Zach Copley <zach@status.net>
113 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
114 * @link http://status.net/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
124 var $auth_user = null;
128 var $since_id = null;
131 var $access = self::READ_ONLY; // read (default) or read-write
133 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
138 * @param array $args Web and URL arguments
140 * @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->page = (int)$this->arg('page', 1);
150 $this->count = (int)$this->arg('count', 20);
151 $this->max_id = (int)$this->arg('max_id', 0);
152 $this->since_id = (int)$this->arg('since_id', 0);
154 if ($this->arg('since')) {
155 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
158 $this->source = $this->trimmed('source');
160 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
161 $this->source = 'api';
170 * @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 $twitter_user['id'] = intval($profile->id);
206 $twitter_user['name'] = $profile->getBestName();
207 $twitter_user['screen_name'] = $profile->nickname;
208 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
209 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
211 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
212 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
213 Avatar::defaultImage(AVATAR_STREAM_SIZE);
215 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
216 $twitter_user['protected'] = false; # not supported by StatusNet yet
217 $twitter_user['followers_count'] = $profile->subscriberCount();
220 $user = $profile->getUser();
222 // Note: some profiles don't have an associated user
224 $defaultDesign = Design::siteDesign();
227 $design = $user->getDesign();
230 if (empty($design)) {
231 $design = $defaultDesign;
234 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
235 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
236 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
237 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
238 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
239 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
240 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
241 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
242 $twitter_user['profile_sidebar_border_color'] = '';
244 $twitter_user['friends_count'] = $profile->subscriptionCount();
246 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
248 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
252 if (!empty($user) && $user->timezone) {
253 $timezone = $user->timezone;
257 $t->setTimezone(new DateTimeZone($timezone));
259 $twitter_user['utc_offset'] = $t->format('Z');
260 $twitter_user['time_zone'] = $timezone;
262 $twitter_user['profile_background_image_url']
263 = empty($design->backgroundimage)
264 ? '' : ($design->disposition & BACKGROUND_ON)
265 ? Design::url($design->backgroundimage) : '';
267 $twitter_user['profile_background_tile']
268 = empty($design->disposition)
269 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
271 $twitter_user['statuses_count'] = $profile->noticeCount();
273 // Is the requesting user following this user?
274 $twitter_user['following'] = false;
275 $twitter_user['notifications'] = false;
277 if (isset($this->auth_user)) {
279 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
282 $sub = Subscription::pkeyGet(array('subscriber' =>
283 $this->auth_user->id,
284 'subscribed' => $profile->id));
287 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
292 $notice = $profile->getCurrentNotice();
295 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
299 return $twitter_user;
302 function twitterStatusArray($notice, $include_user=true)
304 $base = $this->twitterSimpleStatusArray($notice, $include_user);
306 if (!empty($notice->repeat_of)) {
307 $original = Notice::staticGet('id', $notice->repeat_of);
308 if (!empty($original)) {
309 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
310 $base['retweeted_status'] = $original_array;
317 function twitterSimpleStatusArray($notice, $include_user=true)
319 $profile = $notice->getProfile();
321 $twitter_status = array();
322 $twitter_status['text'] = $notice->content;
323 $twitter_status['truncated'] = false; # Not possible on StatusNet
324 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
325 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
326 intval($notice->reply_to) : null;
330 $ns = $notice->getSource();
332 if (!empty($ns->name) && !empty($ns->url)) {
333 $source = '<a href="'
334 . htmlspecialchars($ns->url)
335 . '" rel="nofollow">'
336 . htmlspecialchars($ns->name)
343 $twitter_status['source'] = $source;
344 $twitter_status['id'] = intval($notice->id);
346 $replier_profile = null;
348 if ($notice->reply_to) {
349 $reply = Notice::staticGet(intval($notice->reply_to));
351 $replier_profile = $reply->getProfile();
355 $twitter_status['in_reply_to_user_id'] =
356 ($replier_profile) ? intval($replier_profile->id) : null;
357 $twitter_status['in_reply_to_screen_name'] =
358 ($replier_profile) ? $replier_profile->nickname : null;
360 if (isset($notice->lat) && isset($notice->lon)) {
361 // This is the format that GeoJSON expects stuff to be in
362 $twitter_status['geo'] = array('type' => 'Point',
363 'coordinates' => array((float) $notice->lat,
364 (float) $notice->lon));
366 $twitter_status['geo'] = null;
369 if (isset($this->auth_user)) {
370 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
372 $twitter_status['favorited'] = false;
376 $attachments = $notice->attachments();
378 if (!empty($attachments)) {
380 $twitter_status['attachments'] = array();
382 foreach ($attachments as $attachment) {
383 $enclosure_o=$attachment->getEnclosure();
385 $enclosure = array();
386 $enclosure['url'] = $enclosure_o->url;
387 $enclosure['mimetype'] = $enclosure_o->mimetype;
388 $enclosure['size'] = $enclosure_o->size;
389 $twitter_status['attachments'][] = $enclosure;
394 if ($include_user && $profile) {
395 # Don't get notice (recursive!)
396 $twitter_user = $this->twitterUserArray($profile, false);
397 $twitter_status['user'] = $twitter_user;
400 return $twitter_status;
403 function twitterGroupArray($group)
405 $twitter_group=array();
406 $twitter_group['id']=$group->id;
407 $twitter_group['url']=$group->permalink();
408 $twitter_group['nickname']=$group->nickname;
409 $twitter_group['fullname']=$group->fullname;
410 $twitter_group['original_logo']=$group->original_logo;
411 $twitter_group['homepage_logo']=$group->homepage_logo;
412 $twitter_group['stream_logo']=$group->stream_logo;
413 $twitter_group['mini_logo']=$group->mini_logo;
414 $twitter_group['homepage']=$group->homepage;
415 $twitter_group['description']=$group->description;
416 $twitter_group['location']=$group->location;
417 $twitter_group['created']=$this->dateTwitter($group->created);
418 $twitter_group['modified']=$this->dateTwitter($group->modified);
419 return $twitter_group;
422 function twitterRssGroupArray($group)
425 $entry['content']=$group->description;
426 $entry['title']=$group->nickname;
427 $entry['link']=$group->permalink();
428 $entry['published']=common_date_iso8601($group->created);
429 $entry['updated']==common_date_iso8601($group->modified);
430 $taguribase = common_config('integration', 'groupuri');
431 $entry['id'] = "group:$groupuribase:$entry[link]";
433 $entry['description'] = $entry['content'];
434 $entry['pubDate'] = common_date_rfc2822($group->created);
435 $entry['guid'] = $entry['link'];
440 function twitterRssEntryArray($notice)
442 $profile = $notice->getProfile();
445 // We trim() to avoid extraneous whitespace in the output
447 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
448 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
449 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
450 $entry['published'] = common_date_iso8601($notice->created);
452 $taguribase = TagURI::base();
453 $entry['id'] = "tag:$taguribase:$entry[link]";
455 $entry['updated'] = $entry['published'];
456 $entry['author'] = $profile->getBestName();
459 $attachments = $notice->attachments();
460 $enclosures = array();
462 foreach ($attachments as $attachment) {
463 $enclosure_o=$attachment->getEnclosure();
465 $enclosure = array();
466 $enclosure['url'] = $enclosure_o->url;
467 $enclosure['mimetype'] = $enclosure_o->mimetype;
468 $enclosure['size'] = $enclosure_o->size;
469 $enclosures[] = $enclosure;
473 if (!empty($enclosures)) {
474 $entry['enclosures'] = $enclosures;
478 $tag = new Notice_tag();
479 $tag->notice_id = $notice->id;
481 $entry['tags']=array();
482 while ($tag->fetch()) {
483 $entry['tags'][]=$tag->tag;
489 $entry['description'] = $entry['content'];
490 $entry['pubDate'] = common_date_rfc2822($notice->created);
491 $entry['guid'] = $entry['link'];
493 if (isset($notice->lat) && isset($notice->lon)) {
494 // This is the format that GeoJSON expects stuff to be in.
495 // showGeoRSS() below uses it for XML output, so we reuse it
496 $entry['geo'] = array('type' => 'Point',
497 'coordinates' => array((float) $notice->lat,
498 (float) $notice->lon));
500 $entry['geo'] = null;
506 function twitterRelationshipArray($source, $target)
508 $relationship = array();
510 $relationship['source'] =
511 $this->relationshipDetailsArray($source, $target);
512 $relationship['target'] =
513 $this->relationshipDetailsArray($target, $source);
515 return array('relationship' => $relationship);
518 function relationshipDetailsArray($source, $target)
522 $details['screen_name'] = $source->nickname;
523 $details['followed_by'] = $target->isSubscribed($source);
524 $details['following'] = $source->isSubscribed($target);
526 $notifications = false;
528 if ($source->isSubscribed($target)) {
530 $sub = Subscription::pkeyGet(array('subscriber' =>
531 $source->id, 'subscribed' => $target->id));
534 $notifications = ($sub->jabber || $sub->sms);
538 $details['notifications_enabled'] = $notifications;
539 $details['blocking'] = $source->hasBlocked($target);
540 $details['id'] = $source->id;
545 function showTwitterXmlRelationship($relationship)
547 $this->elementStart('relationship');
549 foreach($relationship as $element => $value) {
550 if ($element == 'source' || $element == 'target') {
551 $this->elementStart($element);
552 $this->showXmlRelationshipDetails($value);
553 $this->elementEnd($element);
557 $this->elementEnd('relationship');
560 function showXmlRelationshipDetails($details)
562 foreach($details as $element => $value) {
563 $this->element($element, null, $value);
567 function showTwitterXmlStatus($twitter_status, $tag='status')
569 $this->elementStart($tag);
570 foreach($twitter_status as $element => $value) {
573 $this->showTwitterXmlUser($twitter_status['user']);
576 $this->element($element, null, common_xml_safe_str($value));
579 $this->showXmlAttachments($twitter_status['attachments']);
582 $this->showGeoXML($value);
584 case 'retweeted_status':
585 $this->showTwitterXmlStatus($value, 'retweeted_status');
588 $this->element($element, null, $value);
591 $this->elementEnd($tag);
594 function showTwitterXmlGroup($twitter_group)
596 $this->elementStart('group');
597 foreach($twitter_group as $element => $value) {
598 $this->element($element, null, $value);
600 $this->elementEnd('group');
603 function showTwitterXmlUser($twitter_user, $role='user')
605 $this->elementStart($role);
606 foreach($twitter_user as $element => $value) {
607 if ($element == 'status') {
608 $this->showTwitterXmlStatus($twitter_user['status']);
610 $this->element($element, null, $value);
613 $this->elementEnd($role);
616 function showXmlAttachments($attachments) {
617 if (!empty($attachments)) {
618 $this->elementStart('attachments', array('type' => 'array'));
619 foreach ($attachments as $attachment) {
621 $attrs['url'] = $attachment['url'];
622 $attrs['mimetype'] = $attachment['mimetype'];
623 $attrs['size'] = $attachment['size'];
624 $this->element('enclosure', $attrs, '');
626 $this->elementEnd('attachments');
630 function showGeoXML($geo)
634 $this->element('geo');
636 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
637 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
638 $this->elementEnd('geo');
642 function showGeoRSS($geo)
648 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
653 function showTwitterRssItem($entry)
655 $this->elementStart('item');
656 $this->element('title', null, $entry['title']);
657 $this->element('description', null, $entry['description']);
658 $this->element('pubDate', null, $entry['pubDate']);
659 $this->element('guid', null, $entry['guid']);
660 $this->element('link', null, $entry['link']);
662 # RSS only supports 1 enclosure per item
663 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
664 $enclosure = $entry['enclosures'][0];
665 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
668 if(array_key_exists('tags', $entry)){
669 foreach($entry['tags'] as $tag){
670 $this->element('category', null,$tag);
674 $this->showGeoRSS($entry['geo']);
675 $this->elementEnd('item');
678 function showJsonObjects($objects)
680 print(json_encode($objects));
683 function showSingleXmlStatus($notice)
685 $this->initDocument('xml');
686 $twitter_status = $this->twitterStatusArray($notice);
687 $this->showTwitterXmlStatus($twitter_status);
688 $this->endDocument('xml');
691 function show_single_json_status($notice)
693 $this->initDocument('json');
694 $status = $this->twitterStatusArray($notice);
695 $this->showJsonObjects($status);
696 $this->endDocument('json');
699 function showXmlTimeline($notice)
702 $this->initDocument('xml');
703 $this->elementStart('statuses', array('type' => 'array'));
705 if (is_array($notice)) {
706 foreach ($notice as $n) {
707 $twitter_status = $this->twitterStatusArray($n);
708 $this->showTwitterXmlStatus($twitter_status);
711 while ($notice->fetch()) {
712 $twitter_status = $this->twitterStatusArray($notice);
713 $this->showTwitterXmlStatus($twitter_status);
717 $this->elementEnd('statuses');
718 $this->endDocument('xml');
721 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
724 $this->initDocument('rss');
726 $this->element('title', null, $title);
727 $this->element('link', null, $link);
729 if (!is_null($self)) {
733 'type' => 'application/rss+xml',
740 if (!is_null($suplink)) {
741 // For FriendFeed's SUP protocol
742 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
743 'rel' => 'http://api.friendfeed.com/2008/03#sup',
745 'type' => 'application/json'));
748 if (!is_null($logo)) {
749 $this->elementStart('image');
750 $this->element('link', null, $link);
751 $this->element('title', null, $title);
752 $this->element('url', null, $logo);
753 $this->elementEnd('image');
756 $this->element('description', null, $subtitle);
757 $this->element('language', null, 'en-us');
758 $this->element('ttl', null, '40');
760 if (is_array($notice)) {
761 foreach ($notice as $n) {
762 $entry = $this->twitterRssEntryArray($n);
763 $this->showTwitterRssItem($entry);
766 while ($notice->fetch()) {
767 $entry = $this->twitterRssEntryArray($notice);
768 $this->showTwitterRssItem($entry);
772 $this->endTwitterRss();
775 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
778 $this->initDocument('atom');
780 $this->element('title', null, $title);
781 $this->element('id', null, $id);
782 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
784 if (!is_null($logo)) {
785 $this->element('logo',null,$logo);
788 if (!is_null($suplink)) {
789 # For FriendFeed's SUP protocol
790 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
792 'type' => 'application/json'));
795 if (!is_null($selfuri)) {
796 $this->element('link', array('href' => $selfuri,
797 'rel' => 'self', 'type' => 'application/atom+xml'), null);
800 $this->element('updated', null, common_date_iso8601('now'));
801 $this->element('subtitle', null, $subtitle);
803 if (is_array($notice)) {
804 foreach ($notice as $n) {
805 $this->raw($n->asAtomEntry());
808 while ($notice->fetch()) {
809 $this->raw($notice->asAtomEntry());
813 $this->endDocument('atom');
817 function showRssGroups($group, $title, $link, $subtitle)
820 $this->initDocument('rss');
822 $this->element('title', null, $title);
823 $this->element('link', null, $link);
824 $this->element('description', null, $subtitle);
825 $this->element('language', null, 'en-us');
826 $this->element('ttl', null, '40');
828 if (is_array($group)) {
829 foreach ($group as $g) {
830 $twitter_group = $this->twitterRssGroupArray($g);
831 $this->showTwitterRssItem($twitter_group);
834 while ($group->fetch()) {
835 $twitter_group = $this->twitterRssGroupArray($group);
836 $this->showTwitterRssItem($twitter_group);
840 $this->endTwitterRss();
843 function showTwitterAtomEntry($entry)
845 $this->elementStart('entry');
846 $this->element('title', null, common_xml_safe_str($entry['title']));
849 array('type' => 'html'),
850 common_xml_safe_str($entry['content'])
852 $this->element('id', null, $entry['id']);
853 $this->element('published', null, $entry['published']);
854 $this->element('updated', null, $entry['updated']);
855 $this->element('link', array('type' => 'text/html',
856 'href' => $entry['link'],
857 'rel' => 'alternate'));
858 $this->element('link', array('type' => $entry['avatar-type'],
859 'href' => $entry['avatar'],
861 $this->elementStart('author');
863 $this->element('name', null, $entry['author-name']);
864 $this->element('uri', null, $entry['author-uri']);
866 $this->elementEnd('author');
867 $this->elementEnd('entry');
870 function showXmlDirectMessage($dm)
872 $this->elementStart('direct_message');
873 foreach($dm as $element => $value) {
877 $this->showTwitterXmlUser($value, $element);
880 $this->element($element, null, common_xml_safe_str($value));
883 $this->element($element, null, $value);
887 $this->elementEnd('direct_message');
890 function directMessageArray($message)
894 $from_profile = $message->getFrom();
895 $to_profile = $message->getTo();
897 $dmsg['id'] = $message->id;
898 $dmsg['sender_id'] = $message->from_profile;
899 $dmsg['text'] = trim($message->content);
900 $dmsg['recipient_id'] = $message->to_profile;
901 $dmsg['created_at'] = $this->dateTwitter($message->created);
902 $dmsg['sender_screen_name'] = $from_profile->nickname;
903 $dmsg['recipient_screen_name'] = $to_profile->nickname;
904 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
905 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
910 function rssDirectMessageArray($message)
914 $from = $message->getFrom();
916 $entry['title'] = sprintf('Message from %1$s to %2$s',
917 $from->nickname, $message->getTo()->nickname);
919 $entry['content'] = common_xml_safe_str($message->rendered);
920 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
921 $entry['published'] = common_date_iso8601($message->created);
923 $taguribase = TagURI::base();
925 $entry['id'] = "tag:$taguribase:$entry[link]";
926 $entry['updated'] = $entry['published'];
928 $entry['author-name'] = $from->getBestName();
929 $entry['author-uri'] = $from->homepage;
931 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
933 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
934 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
938 $entry['description'] = $entry['content'];
939 $entry['pubDate'] = common_date_rfc2822($message->created);
940 $entry['guid'] = $entry['link'];
945 function showSingleXmlDirectMessage($message)
947 $this->initDocument('xml');
948 $dmsg = $this->directMessageArray($message);
949 $this->showXmlDirectMessage($dmsg);
950 $this->endDocument('xml');
953 function showSingleJsonDirectMessage($message)
955 $this->initDocument('json');
956 $dmsg = $this->directMessageArray($message);
957 $this->showJsonObjects($dmsg);
958 $this->endDocument('json');
961 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
964 $this->initDocument('atom');
966 $this->element('title', null, common_xml_safe_str($title));
967 $this->element('id', null, $id);
968 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
970 if (!is_null($selfuri)) {
971 $this->element('link', array('href' => $selfuri,
972 'rel' => 'self', 'type' => 'application/atom+xml'), null);
975 $this->element('updated', null, common_date_iso8601('now'));
976 $this->element('subtitle', null, common_xml_safe_str($subtitle));
978 if (is_array($group)) {
979 foreach ($group as $g) {
980 $this->raw($g->asAtomEntry());
983 while ($group->fetch()) {
984 $this->raw($group->asAtomEntry());
988 $this->endDocument('atom');
992 function showJsonTimeline($notice)
995 $this->initDocument('json');
999 if (is_array($notice)) {
1000 foreach ($notice as $n) {
1001 $twitter_status = $this->twitterStatusArray($n);
1002 array_push($statuses, $twitter_status);
1005 while ($notice->fetch()) {
1006 $twitter_status = $this->twitterStatusArray($notice);
1007 array_push($statuses, $twitter_status);
1011 $this->showJsonObjects($statuses);
1013 $this->endDocument('json');
1016 function showJsonGroups($group)
1019 $this->initDocument('json');
1023 if (is_array($group)) {
1024 foreach ($group as $g) {
1025 $twitter_group = $this->twitterGroupArray($g);
1026 array_push($groups, $twitter_group);
1029 while ($group->fetch()) {
1030 $twitter_group = $this->twitterGroupArray($group);
1031 array_push($groups, $twitter_group);
1035 $this->showJsonObjects($groups);
1037 $this->endDocument('json');
1040 function showXmlGroups($group)
1043 $this->initDocument('xml');
1044 $this->elementStart('groups', array('type' => 'array'));
1046 if (is_array($group)) {
1047 foreach ($group as $g) {
1048 $twitter_group = $this->twitterGroupArray($g);
1049 $this->showTwitterXmlGroup($twitter_group);
1052 while ($group->fetch()) {
1053 $twitter_group = $this->twitterGroupArray($group);
1054 $this->showTwitterXmlGroup($twitter_group);
1058 $this->elementEnd('groups');
1059 $this->endDocument('xml');
1062 function showTwitterXmlUsers($user)
1065 $this->initDocument('xml');
1066 $this->elementStart('users', array('type' => 'array'));
1068 if (is_array($user)) {
1069 foreach ($user as $u) {
1070 $twitter_user = $this->twitterUserArray($u);
1071 $this->showTwitterXmlUser($twitter_user);
1074 while ($user->fetch()) {
1075 $twitter_user = $this->twitterUserArray($user);
1076 $this->showTwitterXmlUser($twitter_user);
1080 $this->elementEnd('users');
1081 $this->endDocument('xml');
1084 function showJsonUsers($user)
1087 $this->initDocument('json');
1091 if (is_array($user)) {
1092 foreach ($user as $u) {
1093 $twitter_user = $this->twitterUserArray($u);
1094 array_push($users, $twitter_user);
1097 while ($user->fetch()) {
1098 $twitter_user = $this->twitterUserArray($user);
1099 array_push($users, $twitter_user);
1103 $this->showJsonObjects($users);
1105 $this->endDocument('json');
1108 function showSingleJsonGroup($group)
1110 $this->initDocument('json');
1111 $twitter_group = $this->twitterGroupArray($group);
1112 $this->showJsonObjects($twitter_group);
1113 $this->endDocument('json');
1116 function showSingleXmlGroup($group)
1118 $this->initDocument('xml');
1119 $twitter_group = $this->twitterGroupArray($group);
1120 $this->showTwitterXmlGroup($twitter_group);
1121 $this->endDocument('xml');
1124 function dateTwitter($dt)
1126 $dateStr = date('d F Y H:i:s', strtotime($dt));
1127 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1128 $d->setTimezone(new DateTimeZone(common_timezone()));
1129 return $d->format('D M d H:i:s O Y');
1132 function initDocument($type='xml')
1136 header('Content-Type: application/xml; charset=utf-8');
1140 header('Content-Type: application/json; charset=utf-8');
1142 // Check for JSONP callback
1143 $callback = $this->arg('callback');
1145 print $callback . '(';
1149 header("Content-Type: application/rss+xml; charset=utf-8");
1150 $this->initTwitterRss();
1153 header('Content-Type: application/atom+xml; charset=utf-8');
1154 $this->initTwitterAtom();
1157 // TRANS: Client error on an API request with an unsupported data format.
1158 $this->clientError(_('Not a supported data format.'));
1165 function endDocument($type='xml')
1173 // Check for JSONP callback
1174 $callback = $this->arg('callback');
1180 $this->endTwitterRss();
1183 $this->endTwitterRss();
1186 // TRANS: Client error on an API request with an unsupported data format.
1187 $this->clientError(_('Not a supported data format.'));
1193 function clientError($msg, $code = 400, $format = 'xml')
1195 $action = $this->trimmed('action');
1197 common_debug("User error '$code' on '$action': $msg", __FILE__);
1199 if (!array_key_exists($code, ClientErrorAction::$status)) {
1203 $status_string = ClientErrorAction::$status[$code];
1205 header('HTTP/1.1 '.$code.' '.$status_string);
1207 if ($format == 'xml') {
1208 $this->initDocument('xml');
1209 $this->elementStart('hash');
1210 $this->element('error', null, $msg);
1211 $this->element('request', null, $_SERVER['REQUEST_URI']);
1212 $this->elementEnd('hash');
1213 $this->endDocument('xml');
1214 } elseif ($format == 'json'){
1215 $this->initDocument('json');
1216 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1217 print(json_encode($error_array));
1218 $this->endDocument('json');
1221 // If user didn't request a useful format, throw a regular client error
1222 throw new ClientException($msg, $code);
1226 function serverError($msg, $code = 500, $content_type = 'xml')
1228 $action = $this->trimmed('action');
1230 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1232 if (!array_key_exists($code, ServerErrorAction::$status)) {
1236 $status_string = ServerErrorAction::$status[$code];
1238 header('HTTP/1.1 '.$code.' '.$status_string);
1240 if ($content_type == 'xml') {
1241 $this->initDocument('xml');
1242 $this->elementStart('hash');
1243 $this->element('error', null, $msg);
1244 $this->element('request', null, $_SERVER['REQUEST_URI']);
1245 $this->elementEnd('hash');
1246 $this->endDocument('xml');
1248 $this->initDocument('json');
1249 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1250 print(json_encode($error_array));
1251 $this->endDocument('json');
1255 function initTwitterRss()
1258 $this->elementStart(
1262 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1263 'xmlns:georss' => 'http://www.georss.org/georss'
1266 $this->elementStart('channel');
1267 Event::handle('StartApiRss', array($this));
1270 function endTwitterRss()
1272 $this->elementEnd('channel');
1273 $this->elementEnd('rss');
1277 function initTwitterAtom()
1280 // FIXME: don't hardcode the language here!
1281 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1282 'xml:lang' => 'en-US',
1283 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1286 function endTwitterAtom()
1288 $this->elementEnd('feed');
1292 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1294 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1295 switch ($content_type) {
1297 $this->showTwitterXmlUser($profile_array);
1300 $this->showJsonObjects($profile_array);
1303 // TRANS: Client error on an API request with an unsupported data format.
1304 $this->clientError(_('Not a supported data format.'));
1310 function getTargetUser($id)
1314 // Twitter supports these other ways of passing the user ID
1315 if (is_numeric($this->arg('id'))) {
1316 return User::staticGet($this->arg('id'));
1317 } else if ($this->arg('id')) {
1318 $nickname = common_canonical_nickname($this->arg('id'));
1319 return User::staticGet('nickname', $nickname);
1320 } else if ($this->arg('user_id')) {
1321 // This is to ensure that a non-numeric user_id still
1322 // overrides screen_name even if it doesn't get used
1323 if (is_numeric($this->arg('user_id'))) {
1324 return User::staticGet('id', $this->arg('user_id'));
1326 } else if ($this->arg('screen_name')) {
1327 $nickname = common_canonical_nickname($this->arg('screen_name'));
1328 return User::staticGet('nickname', $nickname);
1330 // Fall back to trying the currently authenticated user
1331 return $this->auth_user;
1334 } else if (is_numeric($id)) {
1335 return User::staticGet($id);
1337 $nickname = common_canonical_nickname($id);
1338 return User::staticGet('nickname', $nickname);
1342 function getTargetGroup($id)
1345 if (is_numeric($this->arg('id'))) {
1346 return User_group::staticGet($this->arg('id'));
1347 } else if ($this->arg('id')) {
1348 $nickname = common_canonical_nickname($this->arg('id'));
1349 $local = Local_group::staticGet('nickname', $nickname);
1350 if (empty($local)) {
1353 return User_group::staticGet('id', $local->id);
1355 } else if ($this->arg('group_id')) {
1356 // This is to ensure that a non-numeric user_id still
1357 // overrides screen_name even if it doesn't get used
1358 if (is_numeric($this->arg('group_id'))) {
1359 return User_group::staticGet('id', $this->arg('group_id'));
1361 } else if ($this->arg('group_name')) {
1362 $nickname = common_canonical_nickname($this->arg('group_name'));
1363 $local = Local_group::staticGet('nickname', $nickname);
1364 if (empty($local)) {
1367 return User_group::staticGet('id', $local->group_id);
1371 } else if (is_numeric($id)) {
1372 return User_group::staticGet($id);
1374 $nickname = common_canonical_nickname($id);
1375 $local = Local_group::staticGet('nickname', $nickname);
1376 if (empty($local)) {
1379 return User_group::staticGet('id', $local->group_id);
1385 * Returns query argument or default value if not found. Certain
1386 * parameters used throughout the API are lightly scrubbed and
1387 * bounds checked. This overrides Action::arg().
1389 * @param string $key requested argument
1390 * @param string $def default value to return if $key is not provided
1394 function arg($key, $def=null)
1397 // XXX: Do even more input validation/scrubbing?
1399 if (array_key_exists($key, $this->args)) {
1402 $page = (int)$this->args['page'];
1403 return ($page < 1) ? 1 : $page;
1405 $count = (int)$this->args['count'];
1408 } elseif ($count > 200) {
1414 $since_id = (int)$this->args['since_id'];
1415 return ($since_id < 1) ? 0 : $since_id;
1417 $max_id = (int)$this->args['max_id'];
1418 return ($max_id < 1) ? 0 : $max_id;
1420 return parent::arg($key, $def);
1428 * Calculate the complete URI that called up this action. Used for
1429 * Atom rel="self" links. Warning: this is funky.
1431 * @return string URL a URL suitable for rel="self" Atom links
1433 function getSelfUri()
1435 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1437 $id = $this->arg('id');
1438 $aargs = array('format' => $this->format);
1443 $tag = $this->arg('tag');
1445 $aargs['tag'] = $tag;
1448 parse_str($_SERVER['QUERY_STRING'], $params);
1450 if (!empty($params)) {
1451 unset($params['p']);
1452 $pstring = http_build_query($params);
1455 $uri = common_local_url($action, $aargs);
1457 if (!empty($pstring)) {
1458 $uri .= '?' . $pstring;