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 * @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')) {
102 * Contains most of the Twitter-compatible API output functions.
106 * @author Craig Andrews <candrews@integralblue.com>
107 * @author Dan Moore <dan@moore.cx>
108 * @author Evan Prodromou <evan@status.net>
109 * @author Jeffery To <jeffery.to@gmail.com>
110 * @author Toby Inkster <mail@tobyinkster.co.uk>
111 * @author Zach Copley <zach@status.net>
112 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
113 * @link http://status.net/
116 class ApiAction extends Action
119 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
130 var $access = self::READ_ONLY; // read (default) or read-write
132 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
137 * @param array $args Web and URL arguments
139 * @return boolean false if user doesn't exist
142 function prepare($args)
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
148 $this->page = (int)$this->arg('page', 1);
149 $this->count = (int)$this->arg('count', 20);
150 $this->max_id = (int)$this->arg('max_id', 0);
151 $this->since_id = (int)$this->arg('since_id', 0);
153 if ($this->arg('since')) {
154 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
157 $this->source = $this->trimmed('source');
159 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
160 $this->source = 'api';
169 * @param array $args Arguments from $_REQUEST
174 function handle($args)
176 header('Access-Control-Allow-Origin: *');
177 parent::handle($args);
181 * Overrides XMLOutputter::element to write booleans as strings (true|false).
182 * See that method's documentation for more info.
184 * @param string $tag Element type or tagname
185 * @param array $attrs Array of element attributes, as
187 * @param string $content string content of the element
191 function element($tag, $attrs=null, $content=null)
193 if (is_bool($content)) {
194 $content = ($content ? 'true' : 'false');
197 return parent::element($tag, $attrs, $content);
200 function twitterUserArray($profile, $get_notice=false)
202 $twitter_user = array();
204 $twitter_user['id'] = intval($profile->id);
205 $twitter_user['name'] = $profile->getBestName();
206 $twitter_user['screen_name'] = $profile->nickname;
207 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
208 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
210 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
211 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
212 Avatar::defaultImage(AVATAR_STREAM_SIZE);
214 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
215 $twitter_user['protected'] = false; # not supported by StatusNet yet
216 $twitter_user['followers_count'] = $profile->subscriberCount();
219 $user = $profile->getUser();
221 // Note: some profiles don't have an associated user
223 $defaultDesign = Design::siteDesign();
226 $design = $user->getDesign();
229 if (empty($design)) {
230 $design = $defaultDesign;
233 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
234 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
235 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
236 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
238 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
240 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $twitter_user['profile_sidebar_border_color'] = '';
243 $twitter_user['friends_count'] = $profile->subscriptionCount();
245 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
247 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
251 if (!empty($user) && $user->timezone) {
252 $timezone = $user->timezone;
256 $t->setTimezone(new DateTimeZone($timezone));
258 $twitter_user['utc_offset'] = $t->format('Z');
259 $twitter_user['time_zone'] = $timezone;
261 $twitter_user['profile_background_image_url']
262 = empty($design->backgroundimage)
263 ? '' : ($design->disposition & BACKGROUND_ON)
264 ? Design::url($design->backgroundimage) : '';
266 $twitter_user['profile_background_tile']
267 = empty($design->disposition)
268 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
270 $twitter_user['statuses_count'] = $profile->noticeCount();
272 // Is the requesting user following this user?
273 $twitter_user['following'] = false;
274 $twitter_user['notifications'] = false;
276 if (isset($this->auth_user)) {
278 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
281 $sub = Subscription::pkeyGet(array('subscriber' =>
282 $this->auth_user->id,
283 'subscribed' => $profile->id));
286 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
291 $notice = $profile->getCurrentNotice();
294 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
298 return $twitter_user;
301 function twitterStatusArray($notice, $include_user=true)
303 $base = $this->twitterSimpleStatusArray($notice, $include_user);
305 if (!empty($notice->repeat_of)) {
306 $original = Notice::staticGet('id', $notice->repeat_of);
307 if (!empty($original)) {
308 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
309 $base['retweeted_status'] = $original_array;
316 function twitterSimpleStatusArray($notice, $include_user=true)
318 $profile = $notice->getProfile();
320 $twitter_status = array();
321 $twitter_status['text'] = $notice->content;
322 $twitter_status['truncated'] = false; # Not possible on StatusNet
323 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
324 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
325 intval($notice->reply_to) : null;
329 $ns = $notice->getSource();
331 if (!empty($ns->name) && !empty($ns->url)) {
332 $source = '<a href="'
333 . htmlspecialchars($ns->url)
334 . '" rel="nofollow">'
335 . htmlspecialchars($ns->name)
342 $twitter_status['source'] = $source;
343 $twitter_status['id'] = intval($notice->id);
345 $replier_profile = null;
347 if ($notice->reply_to) {
348 $reply = Notice::staticGet(intval($notice->reply_to));
350 $replier_profile = $reply->getProfile();
354 $twitter_status['in_reply_to_user_id'] =
355 ($replier_profile) ? intval($replier_profile->id) : null;
356 $twitter_status['in_reply_to_screen_name'] =
357 ($replier_profile) ? $replier_profile->nickname : null;
359 if (isset($notice->lat) && isset($notice->lon)) {
360 // This is the format that GeoJSON expects stuff to be in
361 $twitter_status['geo'] = array('type' => 'Point',
362 'coordinates' => array((float) $notice->lat,
363 (float) $notice->lon));
365 $twitter_status['geo'] = null;
368 if (isset($this->auth_user)) {
369 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
371 $twitter_status['favorited'] = false;
375 $attachments = $notice->attachments();
377 if (!empty($attachments)) {
379 $twitter_status['attachments'] = array();
381 foreach ($attachments as $attachment) {
382 $enclosure_o=$attachment->getEnclosure();
384 $enclosure = array();
385 $enclosure['url'] = $enclosure_o->url;
386 $enclosure['mimetype'] = $enclosure_o->mimetype;
387 $enclosure['size'] = $enclosure_o->size;
388 $twitter_status['attachments'][] = $enclosure;
393 if ($include_user && $profile) {
394 # Don't get notice (recursive!)
395 $twitter_user = $this->twitterUserArray($profile, false);
396 $twitter_status['user'] = $twitter_user;
399 return $twitter_status;
402 function twitterGroupArray($group)
404 $twitter_group=array();
405 $twitter_group['id']=$group->id;
406 $twitter_group['url']=$group->permalink();
407 $twitter_group['nickname']=$group->nickname;
408 $twitter_group['fullname']=$group->fullname;
409 $twitter_group['original_logo']=$group->original_logo;
410 $twitter_group['homepage_logo']=$group->homepage_logo;
411 $twitter_group['stream_logo']=$group->stream_logo;
412 $twitter_group['mini_logo']=$group->mini_logo;
413 $twitter_group['homepage']=$group->homepage;
414 $twitter_group['description']=$group->description;
415 $twitter_group['location']=$group->location;
416 $twitter_group['created']=$this->dateTwitter($group->created);
417 $twitter_group['modified']=$this->dateTwitter($group->modified);
418 return $twitter_group;
421 function twitterRssGroupArray($group)
424 $entry['content']=$group->description;
425 $entry['title']=$group->nickname;
426 $entry['link']=$group->permalink();
427 $entry['published']=common_date_iso8601($group->created);
428 $entry['updated']==common_date_iso8601($group->modified);
429 $taguribase = common_config('integration', 'groupuri');
430 $entry['id'] = "group:$groupuribase:$entry[link]";
432 $entry['description'] = $entry['content'];
433 $entry['pubDate'] = common_date_rfc2822($group->created);
434 $entry['guid'] = $entry['link'];
439 function twitterRssEntryArray($notice)
441 $profile = $notice->getProfile();
444 // We trim() to avoid extraneous whitespace in the output
446 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
447 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
448 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
449 $entry['published'] = common_date_iso8601($notice->created);
451 $taguribase = TagURI::base();
452 $entry['id'] = "tag:$taguribase:$entry[link]";
454 $entry['updated'] = $entry['published'];
455 $entry['author'] = $profile->getBestName();
458 $attachments = $notice->attachments();
459 $enclosures = array();
461 foreach ($attachments as $attachment) {
462 $enclosure_o=$attachment->getEnclosure();
464 $enclosure = array();
465 $enclosure['url'] = $enclosure_o->url;
466 $enclosure['mimetype'] = $enclosure_o->mimetype;
467 $enclosure['size'] = $enclosure_o->size;
468 $enclosures[] = $enclosure;
472 if (!empty($enclosures)) {
473 $entry['enclosures'] = $enclosures;
477 $tag = new Notice_tag();
478 $tag->notice_id = $notice->id;
480 $entry['tags']=array();
481 while ($tag->fetch()) {
482 $entry['tags'][]=$tag->tag;
488 $entry['description'] = $entry['content'];
489 $entry['pubDate'] = common_date_rfc2822($notice->created);
490 $entry['guid'] = $entry['link'];
492 if (isset($notice->lat) && isset($notice->lon)) {
493 // This is the format that GeoJSON expects stuff to be in.
494 // showGeoRSS() below uses it for XML output, so we reuse it
495 $entry['geo'] = array('type' => 'Point',
496 'coordinates' => array((float) $notice->lat,
497 (float) $notice->lon));
499 $entry['geo'] = null;
505 function twitterRelationshipArray($source, $target)
507 $relationship = array();
509 $relationship['source'] =
510 $this->relationshipDetailsArray($source, $target);
511 $relationship['target'] =
512 $this->relationshipDetailsArray($target, $source);
514 return array('relationship' => $relationship);
517 function relationshipDetailsArray($source, $target)
521 $details['screen_name'] = $source->nickname;
522 $details['followed_by'] = $target->isSubscribed($source);
523 $details['following'] = $source->isSubscribed($target);
525 $notifications = false;
527 if ($source->isSubscribed($target)) {
529 $sub = Subscription::pkeyGet(array('subscriber' =>
530 $source->id, 'subscribed' => $target->id));
533 $notifications = ($sub->jabber || $sub->sms);
537 $details['notifications_enabled'] = $notifications;
538 $details['blocking'] = $source->hasBlocked($target);
539 $details['id'] = $source->id;
544 function showTwitterXmlRelationship($relationship)
546 $this->elementStart('relationship');
548 foreach($relationship as $element => $value) {
549 if ($element == 'source' || $element == 'target') {
550 $this->elementStart($element);
551 $this->showXmlRelationshipDetails($value);
552 $this->elementEnd($element);
556 $this->elementEnd('relationship');
559 function showXmlRelationshipDetails($details)
561 foreach($details as $element => $value) {
562 $this->element($element, null, $value);
566 function showTwitterXmlStatus($twitter_status, $tag='status')
568 $this->elementStart($tag);
569 foreach($twitter_status as $element => $value) {
572 $this->showTwitterXmlUser($twitter_status['user']);
575 $this->element($element, null, common_xml_safe_str($value));
578 $this->showXmlAttachments($twitter_status['attachments']);
581 $this->showGeoXML($value);
583 case 'retweeted_status':
584 $this->showTwitterXmlStatus($value, 'retweeted_status');
587 $this->element($element, null, $value);
590 $this->elementEnd($tag);
593 function showTwitterXmlGroup($twitter_group)
595 $this->elementStart('group');
596 foreach($twitter_group as $element => $value) {
597 $this->element($element, null, $value);
599 $this->elementEnd('group');
602 function showTwitterXmlUser($twitter_user, $role='user')
604 $this->elementStart($role);
605 foreach($twitter_user as $element => $value) {
606 if ($element == 'status') {
607 $this->showTwitterXmlStatus($twitter_user['status']);
609 $this->element($element, null, $value);
612 $this->elementEnd($role);
615 function showXmlAttachments($attachments) {
616 if (!empty($attachments)) {
617 $this->elementStart('attachments', array('type' => 'array'));
618 foreach ($attachments as $attachment) {
620 $attrs['url'] = $attachment['url'];
621 $attrs['mimetype'] = $attachment['mimetype'];
622 $attrs['size'] = $attachment['size'];
623 $this->element('enclosure', $attrs, '');
625 $this->elementEnd('attachments');
629 function showGeoXML($geo)
633 $this->element('geo');
635 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
636 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
637 $this->elementEnd('geo');
641 function showGeoRSS($geo)
647 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
652 function showTwitterRssItem($entry)
654 $this->elementStart('item');
655 $this->element('title', null, $entry['title']);
656 $this->element('description', null, $entry['description']);
657 $this->element('pubDate', null, $entry['pubDate']);
658 $this->element('guid', null, $entry['guid']);
659 $this->element('link', null, $entry['link']);
661 # RSS only supports 1 enclosure per item
662 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
663 $enclosure = $entry['enclosures'][0];
664 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
667 if(array_key_exists('tags', $entry)){
668 foreach($entry['tags'] as $tag){
669 $this->element('category', null,$tag);
673 $this->showGeoRSS($entry['geo']);
674 $this->elementEnd('item');
677 function showJsonObjects($objects)
679 print(json_encode($objects));
682 function showSingleXmlStatus($notice)
684 $this->initDocument('xml');
685 $twitter_status = $this->twitterStatusArray($notice);
686 $this->showTwitterXmlStatus($twitter_status);
687 $this->endDocument('xml');
690 function show_single_json_status($notice)
692 $this->initDocument('json');
693 $status = $this->twitterStatusArray($notice);
694 $this->showJsonObjects($status);
695 $this->endDocument('json');
698 function showXmlTimeline($notice)
701 $this->initDocument('xml');
702 $this->elementStart('statuses', array('type' => 'array'));
704 if (is_array($notice)) {
705 foreach ($notice as $n) {
706 $twitter_status = $this->twitterStatusArray($n);
707 $this->showTwitterXmlStatus($twitter_status);
710 while ($notice->fetch()) {
711 $twitter_status = $this->twitterStatusArray($notice);
712 $this->showTwitterXmlStatus($twitter_status);
716 $this->elementEnd('statuses');
717 $this->endDocument('xml');
720 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
723 $this->initDocument('rss');
725 $this->element('title', null, $title);
726 $this->element('link', null, $link);
728 if (!is_null($self)) {
732 'type' => 'application/rss+xml',
739 if (!is_null($suplink)) {
740 // For FriendFeed's SUP protocol
741 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
742 'rel' => 'http://api.friendfeed.com/2008/03#sup',
744 'type' => 'application/json'));
747 if (!is_null($logo)) {
748 $this->elementStart('image');
749 $this->element('link', null, $link);
750 $this->element('title', null, $title);
751 $this->element('url', null, $logo);
752 $this->elementEnd('image');
755 $this->element('description', null, $subtitle);
756 $this->element('language', null, 'en-us');
757 $this->element('ttl', null, '40');
759 if (is_array($notice)) {
760 foreach ($notice as $n) {
761 $entry = $this->twitterRssEntryArray($n);
762 $this->showTwitterRssItem($entry);
765 while ($notice->fetch()) {
766 $entry = $this->twitterRssEntryArray($notice);
767 $this->showTwitterRssItem($entry);
771 $this->endTwitterRss();
774 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
777 $this->initDocument('atom');
779 $this->element('title', null, $title);
780 $this->element('id', null, $id);
781 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
783 if (!is_null($logo)) {
784 $this->element('logo',null,$logo);
787 if (!is_null($suplink)) {
788 # For FriendFeed's SUP protocol
789 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
791 'type' => 'application/json'));
794 if (!is_null($selfuri)) {
795 $this->element('link', array('href' => $selfuri,
796 'rel' => 'self', 'type' => 'application/atom+xml'), null);
799 $this->element('updated', null, common_date_iso8601('now'));
800 $this->element('subtitle', null, $subtitle);
802 if (is_array($notice)) {
803 foreach ($notice as $n) {
804 $this->raw($n->asAtomEntry());
807 while ($notice->fetch()) {
808 $this->raw($notice->asAtomEntry());
812 $this->endDocument('atom');
816 function showRssGroups($group, $title, $link, $subtitle)
819 $this->initDocument('rss');
821 $this->element('title', null, $title);
822 $this->element('link', null, $link);
823 $this->element('description', null, $subtitle);
824 $this->element('language', null, 'en-us');
825 $this->element('ttl', null, '40');
827 if (is_array($group)) {
828 foreach ($group as $g) {
829 $twitter_group = $this->twitterRssGroupArray($g);
830 $this->showTwitterRssItem($twitter_group);
833 while ($group->fetch()) {
834 $twitter_group = $this->twitterRssGroupArray($group);
835 $this->showTwitterRssItem($twitter_group);
839 $this->endTwitterRss();
842 function showTwitterAtomEntry($entry)
844 $this->elementStart('entry');
845 $this->element('title', null, common_xml_safe_str($entry['title']));
848 array('type' => 'html'),
849 common_xml_safe_str($entry['content'])
851 $this->element('id', null, $entry['id']);
852 $this->element('published', null, $entry['published']);
853 $this->element('updated', null, $entry['updated']);
854 $this->element('link', array('type' => 'text/html',
855 'href' => $entry['link'],
856 'rel' => 'alternate'));
857 $this->element('link', array('type' => $entry['avatar-type'],
858 'href' => $entry['avatar'],
860 $this->elementStart('author');
862 $this->element('name', null, $entry['author-name']);
863 $this->element('uri', null, $entry['author-uri']);
865 $this->elementEnd('author');
866 $this->elementEnd('entry');
869 function showXmlDirectMessage($dm)
871 $this->elementStart('direct_message');
872 foreach($dm as $element => $value) {
876 $this->showTwitterXmlUser($value, $element);
879 $this->element($element, null, common_xml_safe_str($value));
882 $this->element($element, null, $value);
886 $this->elementEnd('direct_message');
889 function directMessageArray($message)
893 $from_profile = $message->getFrom();
894 $to_profile = $message->getTo();
896 $dmsg['id'] = $message->id;
897 $dmsg['sender_id'] = $message->from_profile;
898 $dmsg['text'] = trim($message->content);
899 $dmsg['recipient_id'] = $message->to_profile;
900 $dmsg['created_at'] = $this->dateTwitter($message->created);
901 $dmsg['sender_screen_name'] = $from_profile->nickname;
902 $dmsg['recipient_screen_name'] = $to_profile->nickname;
903 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
904 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
909 function rssDirectMessageArray($message)
913 $from = $message->getFrom();
915 $entry['title'] = sprintf('Message from %1$s to %2$s',
916 $from->nickname, $message->getTo()->nickname);
918 $entry['content'] = common_xml_safe_str($message->rendered);
919 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
920 $entry['published'] = common_date_iso8601($message->created);
922 $taguribase = TagURI::base();
924 $entry['id'] = "tag:$taguribase:$entry[link]";
925 $entry['updated'] = $entry['published'];
927 $entry['author-name'] = $from->getBestName();
928 $entry['author-uri'] = $from->homepage;
930 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
932 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
933 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
937 $entry['description'] = $entry['content'];
938 $entry['pubDate'] = common_date_rfc2822($message->created);
939 $entry['guid'] = $entry['link'];
944 function showSingleXmlDirectMessage($message)
946 $this->initDocument('xml');
947 $dmsg = $this->directMessageArray($message);
948 $this->showXmlDirectMessage($dmsg);
949 $this->endDocument('xml');
952 function showSingleJsonDirectMessage($message)
954 $this->initDocument('json');
955 $dmsg = $this->directMessageArray($message);
956 $this->showJsonObjects($dmsg);
957 $this->endDocument('json');
960 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
963 $this->initDocument('atom');
965 $this->element('title', null, common_xml_safe_str($title));
966 $this->element('id', null, $id);
967 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
969 if (!is_null($selfuri)) {
970 $this->element('link', array('href' => $selfuri,
971 'rel' => 'self', 'type' => 'application/atom+xml'), null);
974 $this->element('updated', null, common_date_iso8601('now'));
975 $this->element('subtitle', null, common_xml_safe_str($subtitle));
977 if (is_array($group)) {
978 foreach ($group as $g) {
979 $this->raw($g->asAtomEntry());
982 while ($group->fetch()) {
983 $this->raw($group->asAtomEntry());
987 $this->endDocument('atom');
991 function showJsonTimeline($notice)
994 $this->initDocument('json');
998 if (is_array($notice)) {
999 foreach ($notice as $n) {
1000 $twitter_status = $this->twitterStatusArray($n);
1001 array_push($statuses, $twitter_status);
1004 while ($notice->fetch()) {
1005 $twitter_status = $this->twitterStatusArray($notice);
1006 array_push($statuses, $twitter_status);
1010 $this->showJsonObjects($statuses);
1012 $this->endDocument('json');
1015 function showJsonGroups($group)
1018 $this->initDocument('json');
1022 if (is_array($group)) {
1023 foreach ($group as $g) {
1024 $twitter_group = $this->twitterGroupArray($g);
1025 array_push($groups, $twitter_group);
1028 while ($group->fetch()) {
1029 $twitter_group = $this->twitterGroupArray($group);
1030 array_push($groups, $twitter_group);
1034 $this->showJsonObjects($groups);
1036 $this->endDocument('json');
1039 function showXmlGroups($group)
1042 $this->initDocument('xml');
1043 $this->elementStart('groups', array('type' => 'array'));
1045 if (is_array($group)) {
1046 foreach ($group as $g) {
1047 $twitter_group = $this->twitterGroupArray($g);
1048 $this->showTwitterXmlGroup($twitter_group);
1051 while ($group->fetch()) {
1052 $twitter_group = $this->twitterGroupArray($group);
1053 $this->showTwitterXmlGroup($twitter_group);
1057 $this->elementEnd('groups');
1058 $this->endDocument('xml');
1061 function showTwitterXmlUsers($user)
1064 $this->initDocument('xml');
1065 $this->elementStart('users', array('type' => 'array'));
1067 if (is_array($user)) {
1068 foreach ($user as $u) {
1069 $twitter_user = $this->twitterUserArray($u);
1070 $this->showTwitterXmlUser($twitter_user);
1073 while ($user->fetch()) {
1074 $twitter_user = $this->twitterUserArray($user);
1075 $this->showTwitterXmlUser($twitter_user);
1079 $this->elementEnd('users');
1080 $this->endDocument('xml');
1083 function showJsonUsers($user)
1086 $this->initDocument('json');
1090 if (is_array($user)) {
1091 foreach ($user as $u) {
1092 $twitter_user = $this->twitterUserArray($u);
1093 array_push($users, $twitter_user);
1096 while ($user->fetch()) {
1097 $twitter_user = $this->twitterUserArray($user);
1098 array_push($users, $twitter_user);
1102 $this->showJsonObjects($users);
1104 $this->endDocument('json');
1107 function showSingleJsonGroup($group)
1109 $this->initDocument('json');
1110 $twitter_group = $this->twitterGroupArray($group);
1111 $this->showJsonObjects($twitter_group);
1112 $this->endDocument('json');
1115 function showSingleXmlGroup($group)
1117 $this->initDocument('xml');
1118 $twitter_group = $this->twitterGroupArray($group);
1119 $this->showTwitterXmlGroup($twitter_group);
1120 $this->endDocument('xml');
1123 function dateTwitter($dt)
1125 $dateStr = date('d F Y H:i:s', strtotime($dt));
1126 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1127 $d->setTimezone(new DateTimeZone(common_timezone()));
1128 return $d->format('D M d H:i:s O Y');
1131 function initDocument($type='xml')
1135 header('Content-Type: application/xml; charset=utf-8');
1139 header('Content-Type: application/json; charset=utf-8');
1141 // Check for JSONP callback
1142 $callback = $this->arg('callback');
1144 print $callback . '(';
1148 header("Content-Type: application/rss+xml; charset=utf-8");
1149 $this->initTwitterRss();
1152 header('Content-Type: application/atom+xml; charset=utf-8');
1153 $this->initTwitterAtom();
1156 // TRANS: Client error on an API request with an unsupported data format.
1157 $this->clientError(_('Not a supported data format.'));
1164 function endDocument($type='xml')
1172 // Check for JSONP callback
1173 $callback = $this->arg('callback');
1179 $this->endTwitterRss();
1182 $this->endTwitterRss();
1185 // TRANS: Client error on an API request with an unsupported data format.
1186 $this->clientError(_('Not a supported data format.'));
1192 function clientError($msg, $code = 400, $format = 'xml')
1194 $action = $this->trimmed('action');
1196 common_debug("User error '$code' on '$action': $msg", __FILE__);
1198 if (!array_key_exists($code, ClientErrorAction::$status)) {
1202 $status_string = ClientErrorAction::$status[$code];
1204 header('HTTP/1.1 '.$code.' '.$status_string);
1206 if ($format == 'xml') {
1207 $this->initDocument('xml');
1208 $this->elementStart('hash');
1209 $this->element('error', null, $msg);
1210 $this->element('request', null, $_SERVER['REQUEST_URI']);
1211 $this->elementEnd('hash');
1212 $this->endDocument('xml');
1213 } elseif ($format == 'json'){
1214 $this->initDocument('json');
1215 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1216 print(json_encode($error_array));
1217 $this->endDocument('json');
1220 // If user didn't request a useful format, throw a regular client error
1221 throw new ClientException($msg, $code);
1225 function serverError($msg, $code = 500, $content_type = 'xml')
1227 $action = $this->trimmed('action');
1229 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1231 if (!array_key_exists($code, ServerErrorAction::$status)) {
1235 $status_string = ServerErrorAction::$status[$code];
1237 header('HTTP/1.1 '.$code.' '.$status_string);
1239 if ($content_type == 'xml') {
1240 $this->initDocument('xml');
1241 $this->elementStart('hash');
1242 $this->element('error', null, $msg);
1243 $this->element('request', null, $_SERVER['REQUEST_URI']);
1244 $this->elementEnd('hash');
1245 $this->endDocument('xml');
1247 $this->initDocument('json');
1248 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1249 print(json_encode($error_array));
1250 $this->endDocument('json');
1254 function initTwitterRss()
1257 $this->elementStart(
1261 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1262 'xmlns:georss' => 'http://www.georss.org/georss'
1265 $this->elementStart('channel');
1266 Event::handle('StartApiRss', array($this));
1269 function endTwitterRss()
1271 $this->elementEnd('channel');
1272 $this->elementEnd('rss');
1276 function initTwitterAtom()
1279 // FIXME: don't hardcode the language here!
1280 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1281 'xml:lang' => 'en-US',
1282 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1285 function endTwitterAtom()
1287 $this->elementEnd('feed');
1291 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1293 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1294 switch ($content_type) {
1296 $this->showTwitterXmlUser($profile_array);
1299 $this->showJsonObjects($profile_array);
1302 // TRANS: Client error on an API request with an unsupported data format.
1303 $this->clientError(_('Not a supported data format.'));
1309 function getTargetUser($id)
1313 // Twitter supports these other ways of passing the user ID
1314 if (is_numeric($this->arg('id'))) {
1315 return User::staticGet($this->arg('id'));
1316 } else if ($this->arg('id')) {
1317 $nickname = common_canonical_nickname($this->arg('id'));
1318 return User::staticGet('nickname', $nickname);
1319 } else if ($this->arg('user_id')) {
1320 // This is to ensure that a non-numeric user_id still
1321 // overrides screen_name even if it doesn't get used
1322 if (is_numeric($this->arg('user_id'))) {
1323 return User::staticGet('id', $this->arg('user_id'));
1325 } else if ($this->arg('screen_name')) {
1326 $nickname = common_canonical_nickname($this->arg('screen_name'));
1327 return User::staticGet('nickname', $nickname);
1329 // Fall back to trying the currently authenticated user
1330 return $this->auth_user;
1333 } else if (is_numeric($id)) {
1334 return User::staticGet($id);
1336 $nickname = common_canonical_nickname($id);
1337 return User::staticGet('nickname', $nickname);
1341 function getTargetGroup($id)
1344 if (is_numeric($this->arg('id'))) {
1345 return User_group::staticGet($this->arg('id'));
1346 } else if ($this->arg('id')) {
1347 $nickname = common_canonical_nickname($this->arg('id'));
1348 $local = Local_group::staticGet('nickname', $nickname);
1349 if (empty($local)) {
1352 return User_group::staticGet('id', $local->id);
1354 } else if ($this->arg('group_id')) {
1355 // This is to ensure that a non-numeric user_id still
1356 // overrides screen_name even if it doesn't get used
1357 if (is_numeric($this->arg('group_id'))) {
1358 return User_group::staticGet('id', $this->arg('group_id'));
1360 } else if ($this->arg('group_name')) {
1361 $nickname = common_canonical_nickname($this->arg('group_name'));
1362 $local = Local_group::staticGet('nickname', $nickname);
1363 if (empty($local)) {
1366 return User_group::staticGet('id', $local->group_id);
1370 } else if (is_numeric($id)) {
1371 return User_group::staticGet($id);
1373 $nickname = common_canonical_nickname($id);
1374 $local = Local_group::staticGet('nickname', $nickname);
1375 if (empty($local)) {
1378 return User_group::staticGet('id', $local->group_id);
1384 * Returns query argument or default value if not found. Certain
1385 * parameters used throughout the API are lightly scrubbed and
1386 * bounds checked. This overrides Action::arg().
1388 * @param string $key requested argument
1389 * @param string $def default value to return if $key is not provided
1393 function arg($key, $def=null)
1396 // XXX: Do even more input validation/scrubbing?
1398 if (array_key_exists($key, $this->args)) {
1401 $page = (int)$this->args['page'];
1402 return ($page < 1) ? 1 : $page;
1404 $count = (int)$this->args['count'];
1407 } elseif ($count > 200) {
1413 $since_id = (int)$this->args['since_id'];
1414 return ($since_id < 1) ? 0 : $since_id;
1416 $max_id = (int)$this->args['max_id'];
1417 return ($max_id < 1) ? 0 : $max_id;
1419 return parent::arg($key, $def);
1427 * Calculate the complete URI that called up this action. Used for
1428 * Atom rel="self" links. Warning: this is funky.
1430 * @return string URL a URL suitable for rel="self" Atom links
1432 function getSelfUri()
1434 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1436 $id = $this->arg('id');
1437 $aargs = array('format' => $this->format);
1442 $tag = $this->arg('tag');
1444 $aargs['tag'] = $tag;
1447 parse_str($_SERVER['QUERY_STRING'], $params);
1449 if (!empty($params)) {
1450 unset($params['p']);
1451 $pstring = http_build_query($params);
1454 $uri = common_local_url($action, $aargs);
1456 if (!empty($pstring)) {
1457 $uri .= '?' . $pstring;