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;
130 var $access = self::READ_ONLY; // read (default) or read-write
135 * @param array $args Web and URL arguments
137 * @return boolean false if user doesn't exist
140 function prepare($args)
142 StatusNet::setApi(true); // reduce exception reports to aid in debugging
143 parent::prepare($args);
145 $this->format = $this->arg('format');
146 $this->page = (int)$this->arg('page', 1);
147 $this->count = (int)$this->arg('count', 20);
148 $this->max_id = (int)$this->arg('max_id', 0);
149 $this->since_id = (int)$this->arg('since_id', 0);
151 if ($this->arg('since')) {
152 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
161 * @param array $args Arguments from $_REQUEST
166 function handle($args)
168 header('Access-Control-Allow-Origin: *');
169 parent::handle($args);
173 * Overrides XMLOutputter::element to write booleans as strings (true|false).
174 * See that method's documentation for more info.
176 * @param string $tag Element type or tagname
177 * @param array $attrs Array of element attributes, as
179 * @param string $content string content of the element
183 function element($tag, $attrs=null, $content=null)
185 if (is_bool($content)) {
186 $content = ($content ? 'true' : 'false');
189 return parent::element($tag, $attrs, $content);
192 function twitterUserArray($profile, $get_notice=false)
194 $twitter_user = array();
196 $twitter_user['id'] = intval($profile->id);
197 $twitter_user['name'] = $profile->getBestName();
198 $twitter_user['screen_name'] = $profile->nickname;
199 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
200 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
202 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
203 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
204 Avatar::defaultImage(AVATAR_STREAM_SIZE);
206 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
207 $twitter_user['protected'] = false; # not supported by StatusNet yet
208 $twitter_user['followers_count'] = $profile->subscriberCount();
211 $user = $profile->getUser();
213 // Note: some profiles don't have an associated user
215 $defaultDesign = Design::siteDesign();
218 $design = $user->getDesign();
221 if (empty($design)) {
222 $design = $defaultDesign;
225 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
226 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
227 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
228 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
229 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
230 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
231 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
232 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
233 $twitter_user['profile_sidebar_border_color'] = '';
235 $twitter_user['friends_count'] = $profile->subscriptionCount();
237 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
239 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
243 if (!empty($user) && $user->timezone) {
244 $timezone = $user->timezone;
248 $t->setTimezone(new DateTimeZone($timezone));
250 $twitter_user['utc_offset'] = $t->format('Z');
251 $twitter_user['time_zone'] = $timezone;
253 $twitter_user['profile_background_image_url']
254 = empty($design->backgroundimage)
255 ? '' : ($design->disposition & BACKGROUND_ON)
256 ? Design::url($design->backgroundimage) : '';
258 $twitter_user['profile_background_tile']
259 = empty($design->disposition)
260 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
262 $twitter_user['statuses_count'] = $profile->noticeCount();
264 // Is the requesting user following this user?
265 $twitter_user['following'] = false;
266 $twitter_user['notifications'] = false;
268 if (isset($this->auth_user)) {
270 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
273 $sub = Subscription::pkeyGet(array('subscriber' =>
274 $this->auth_user->id,
275 'subscribed' => $profile->id));
278 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
283 $notice = $profile->getCurrentNotice();
286 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
290 return $twitter_user;
293 function twitterStatusArray($notice, $include_user=true)
295 $base = $this->twitterSimpleStatusArray($notice, $include_user);
297 if (!empty($notice->repeat_of)) {
298 $original = Notice::staticGet('id', $notice->repeat_of);
299 if (!empty($original)) {
300 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
301 $base['retweeted_status'] = $original_array;
308 function twitterSimpleStatusArray($notice, $include_user=true)
310 $profile = $notice->getProfile();
312 $twitter_status = array();
313 $twitter_status['text'] = $notice->content;
314 $twitter_status['truncated'] = false; # Not possible on StatusNet
315 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
316 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
317 intval($notice->reply_to) : null;
321 $ns = $notice->getSource();
323 if (!empty($ns->name) && !empty($ns->url)) {
324 $source = '<a href="'
325 . htmlspecialchars($ns->url)
326 . '" rel="nofollow">'
327 . htmlspecialchars($ns->name)
334 $twitter_status['source'] = $source;
335 $twitter_status['id'] = intval($notice->id);
337 $replier_profile = null;
339 if ($notice->reply_to) {
340 $reply = Notice::staticGet(intval($notice->reply_to));
342 $replier_profile = $reply->getProfile();
346 $twitter_status['in_reply_to_user_id'] =
347 ($replier_profile) ? intval($replier_profile->id) : null;
348 $twitter_status['in_reply_to_screen_name'] =
349 ($replier_profile) ? $replier_profile->nickname : null;
351 if (isset($notice->lat) && isset($notice->lon)) {
352 // This is the format that GeoJSON expects stuff to be in
353 $twitter_status['geo'] = array('type' => 'Point',
354 'coordinates' => array((float) $notice->lat,
355 (float) $notice->lon));
357 $twitter_status['geo'] = null;
360 if (isset($this->auth_user)) {
361 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
363 $twitter_status['favorited'] = false;
367 $attachments = $notice->attachments();
369 if (!empty($attachments)) {
371 $twitter_status['attachments'] = array();
373 foreach ($attachments as $attachment) {
374 $enclosure_o=$attachment->getEnclosure();
376 $enclosure = array();
377 $enclosure['url'] = $enclosure_o->url;
378 $enclosure['mimetype'] = $enclosure_o->mimetype;
379 $enclosure['size'] = $enclosure_o->size;
380 $twitter_status['attachments'][] = $enclosure;
385 if ($include_user && $profile) {
386 # Don't get notice (recursive!)
387 $twitter_user = $this->twitterUserArray($profile, false);
388 $twitter_status['user'] = $twitter_user;
391 return $twitter_status;
394 function twitterGroupArray($group)
396 $twitter_group=array();
397 $twitter_group['id']=$group->id;
398 $twitter_group['url']=$group->permalink();
399 $twitter_group['nickname']=$group->nickname;
400 $twitter_group['fullname']=$group->fullname;
401 $twitter_group['original_logo']=$group->original_logo;
402 $twitter_group['homepage_logo']=$group->homepage_logo;
403 $twitter_group['stream_logo']=$group->stream_logo;
404 $twitter_group['mini_logo']=$group->mini_logo;
405 $twitter_group['homepage']=$group->homepage;
406 $twitter_group['description']=$group->description;
407 $twitter_group['location']=$group->location;
408 $twitter_group['created']=$this->dateTwitter($group->created);
409 $twitter_group['modified']=$this->dateTwitter($group->modified);
410 return $twitter_group;
413 function twitterRssGroupArray($group)
416 $entry['content']=$group->description;
417 $entry['title']=$group->nickname;
418 $entry['link']=$group->permalink();
419 $entry['published']=common_date_iso8601($group->created);
420 $entry['updated']==common_date_iso8601($group->modified);
421 $taguribase = common_config('integration', 'groupuri');
422 $entry['id'] = "group:$groupuribase:$entry[link]";
424 $entry['description'] = $entry['content'];
425 $entry['pubDate'] = common_date_rfc2822($group->created);
426 $entry['guid'] = $entry['link'];
431 function twitterRssEntryArray($notice)
433 $profile = $notice->getProfile();
436 // We trim() to avoid extraneous whitespace in the output
438 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
439 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
440 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
441 $entry['published'] = common_date_iso8601($notice->created);
443 $taguribase = TagURI::base();
444 $entry['id'] = "tag:$taguribase:$entry[link]";
446 $entry['updated'] = $entry['published'];
447 $entry['author'] = $profile->getBestName();
450 $attachments = $notice->attachments();
451 $enclosures = array();
453 foreach ($attachments as $attachment) {
454 $enclosure_o=$attachment->getEnclosure();
456 $enclosure = array();
457 $enclosure['url'] = $enclosure_o->url;
458 $enclosure['mimetype'] = $enclosure_o->mimetype;
459 $enclosure['size'] = $enclosure_o->size;
460 $enclosures[] = $enclosure;
464 if (!empty($enclosures)) {
465 $entry['enclosures'] = $enclosures;
469 $tag = new Notice_tag();
470 $tag->notice_id = $notice->id;
472 $entry['tags']=array();
473 while ($tag->fetch()) {
474 $entry['tags'][]=$tag->tag;
480 $entry['description'] = $entry['content'];
481 $entry['pubDate'] = common_date_rfc2822($notice->created);
482 $entry['guid'] = $entry['link'];
484 if (isset($notice->lat) && isset($notice->lon)) {
485 // This is the format that GeoJSON expects stuff to be in.
486 // showGeoRSS() below uses it for XML output, so we reuse it
487 $entry['geo'] = array('type' => 'Point',
488 'coordinates' => array((float) $notice->lat,
489 (float) $notice->lon));
491 $entry['geo'] = null;
497 function twitterRelationshipArray($source, $target)
499 $relationship = array();
501 $relationship['source'] =
502 $this->relationshipDetailsArray($source, $target);
503 $relationship['target'] =
504 $this->relationshipDetailsArray($target, $source);
506 return array('relationship' => $relationship);
509 function relationshipDetailsArray($source, $target)
513 $details['screen_name'] = $source->nickname;
514 $details['followed_by'] = $target->isSubscribed($source);
515 $details['following'] = $source->isSubscribed($target);
517 $notifications = false;
519 if ($source->isSubscribed($target)) {
521 $sub = Subscription::pkeyGet(array('subscriber' =>
522 $source->id, 'subscribed' => $target->id));
525 $notifications = ($sub->jabber || $sub->sms);
529 $details['notifications_enabled'] = $notifications;
530 $details['blocking'] = $source->hasBlocked($target);
531 $details['id'] = $source->id;
536 function showTwitterXmlRelationship($relationship)
538 $this->elementStart('relationship');
540 foreach($relationship as $element => $value) {
541 if ($element == 'source' || $element == 'target') {
542 $this->elementStart($element);
543 $this->showXmlRelationshipDetails($value);
544 $this->elementEnd($element);
548 $this->elementEnd('relationship');
551 function showXmlRelationshipDetails($details)
553 foreach($details as $element => $value) {
554 $this->element($element, null, $value);
558 function showTwitterXmlStatus($twitter_status, $tag='status')
560 $this->elementStart($tag);
561 foreach($twitter_status as $element => $value) {
564 $this->showTwitterXmlUser($twitter_status['user']);
567 $this->element($element, null, common_xml_safe_str($value));
570 $this->showXmlAttachments($twitter_status['attachments']);
573 $this->showGeoXML($value);
575 case 'retweeted_status':
576 $this->showTwitterXmlStatus($value, 'retweeted_status');
579 $this->element($element, null, $value);
582 $this->elementEnd($tag);
585 function showTwitterXmlGroup($twitter_group)
587 $this->elementStart('group');
588 foreach($twitter_group as $element => $value) {
589 $this->element($element, null, $value);
591 $this->elementEnd('group');
594 function showTwitterXmlUser($twitter_user, $role='user')
596 $this->elementStart($role);
597 foreach($twitter_user as $element => $value) {
598 if ($element == 'status') {
599 $this->showTwitterXmlStatus($twitter_user['status']);
601 $this->element($element, null, $value);
604 $this->elementEnd($role);
607 function showXmlAttachments($attachments) {
608 if (!empty($attachments)) {
609 $this->elementStart('attachments', array('type' => 'array'));
610 foreach ($attachments as $attachment) {
612 $attrs['url'] = $attachment['url'];
613 $attrs['mimetype'] = $attachment['mimetype'];
614 $attrs['size'] = $attachment['size'];
615 $this->element('enclosure', $attrs, '');
617 $this->elementEnd('attachments');
621 function showGeoXML($geo)
625 $this->element('geo');
627 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
628 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
629 $this->elementEnd('geo');
633 function showGeoRSS($geo)
639 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
644 function showTwitterRssItem($entry)
646 $this->elementStart('item');
647 $this->element('title', null, $entry['title']);
648 $this->element('description', null, $entry['description']);
649 $this->element('pubDate', null, $entry['pubDate']);
650 $this->element('guid', null, $entry['guid']);
651 $this->element('link', null, $entry['link']);
653 # RSS only supports 1 enclosure per item
654 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
655 $enclosure = $entry['enclosures'][0];
656 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
659 if(array_key_exists('tags', $entry)){
660 foreach($entry['tags'] as $tag){
661 $this->element('category', null,$tag);
665 $this->showGeoRSS($entry['geo']);
666 $this->elementEnd('item');
669 function showJsonObjects($objects)
671 print(json_encode($objects));
674 function showSingleXmlStatus($notice)
676 $this->initDocument('xml');
677 $twitter_status = $this->twitterStatusArray($notice);
678 $this->showTwitterXmlStatus($twitter_status);
679 $this->endDocument('xml');
682 function show_single_json_status($notice)
684 $this->initDocument('json');
685 $status = $this->twitterStatusArray($notice);
686 $this->showJsonObjects($status);
687 $this->endDocument('json');
690 function showXmlTimeline($notice)
693 $this->initDocument('xml');
694 $this->elementStart('statuses', array('type' => 'array'));
696 if (is_array($notice)) {
697 foreach ($notice as $n) {
698 $twitter_status = $this->twitterStatusArray($n);
699 $this->showTwitterXmlStatus($twitter_status);
702 while ($notice->fetch()) {
703 $twitter_status = $this->twitterStatusArray($notice);
704 $this->showTwitterXmlStatus($twitter_status);
708 $this->elementEnd('statuses');
709 $this->endDocument('xml');
712 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
715 $this->initDocument('rss');
717 $this->element('title', null, $title);
718 $this->element('link', null, $link);
720 if (!is_null($self)) {
724 'type' => 'application/rss+xml',
731 if (!is_null($suplink)) {
732 // For FriendFeed's SUP protocol
733 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
734 'rel' => 'http://api.friendfeed.com/2008/03#sup',
736 'type' => 'application/json'));
739 if (!is_null($logo)) {
740 $this->elementStart('image');
741 $this->element('link', null, $link);
742 $this->element('title', null, $title);
743 $this->element('url', null, $logo);
744 $this->elementEnd('image');
747 $this->element('description', null, $subtitle);
748 $this->element('language', null, 'en-us');
749 $this->element('ttl', null, '40');
751 if (is_array($notice)) {
752 foreach ($notice as $n) {
753 $entry = $this->twitterRssEntryArray($n);
754 $this->showTwitterRssItem($entry);
757 while ($notice->fetch()) {
758 $entry = $this->twitterRssEntryArray($notice);
759 $this->showTwitterRssItem($entry);
763 $this->endTwitterRss();
766 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
769 $this->initDocument('atom');
771 $this->element('title', null, $title);
772 $this->element('id', null, $id);
773 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
775 if (!is_null($logo)) {
776 $this->element('logo',null,$logo);
779 if (!is_null($suplink)) {
780 # For FriendFeed's SUP protocol
781 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
783 'type' => 'application/json'));
786 if (!is_null($selfuri)) {
787 $this->element('link', array('href' => $selfuri,
788 'rel' => 'self', 'type' => 'application/atom+xml'), null);
791 $this->element('updated', null, common_date_iso8601('now'));
792 $this->element('subtitle', null, $subtitle);
794 if (is_array($notice)) {
795 foreach ($notice as $n) {
796 $this->raw($n->asAtomEntry());
799 while ($notice->fetch()) {
800 $this->raw($notice->asAtomEntry());
804 $this->endDocument('atom');
808 function showRssGroups($group, $title, $link, $subtitle)
811 $this->initDocument('rss');
813 $this->element('title', null, $title);
814 $this->element('link', null, $link);
815 $this->element('description', null, $subtitle);
816 $this->element('language', null, 'en-us');
817 $this->element('ttl', null, '40');
819 if (is_array($group)) {
820 foreach ($group as $g) {
821 $twitter_group = $this->twitterRssGroupArray($g);
822 $this->showTwitterRssItem($twitter_group);
825 while ($group->fetch()) {
826 $twitter_group = $this->twitterRssGroupArray($group);
827 $this->showTwitterRssItem($twitter_group);
831 $this->endTwitterRss();
834 function showTwitterAtomEntry($entry)
836 $this->elementStart('entry');
837 $this->element('title', null, common_xml_safe_str($entry['title']));
840 array('type' => 'html'),
841 common_xml_safe_str($entry['content'])
843 $this->element('id', null, $entry['id']);
844 $this->element('published', null, $entry['published']);
845 $this->element('updated', null, $entry['updated']);
846 $this->element('link', array('type' => 'text/html',
847 'href' => $entry['link'],
848 'rel' => 'alternate'));
849 $this->element('link', array('type' => $entry['avatar-type'],
850 'href' => $entry['avatar'],
852 $this->elementStart('author');
854 $this->element('name', null, $entry['author-name']);
855 $this->element('uri', null, $entry['author-uri']);
857 $this->elementEnd('author');
858 $this->elementEnd('entry');
861 function showXmlDirectMessage($dm)
863 $this->elementStart('direct_message');
864 foreach($dm as $element => $value) {
868 $this->showTwitterXmlUser($value, $element);
871 $this->element($element, null, common_xml_safe_str($value));
874 $this->element($element, null, $value);
878 $this->elementEnd('direct_message');
881 function directMessageArray($message)
885 $from_profile = $message->getFrom();
886 $to_profile = $message->getTo();
888 $dmsg['id'] = $message->id;
889 $dmsg['sender_id'] = $message->from_profile;
890 $dmsg['text'] = trim($message->content);
891 $dmsg['recipient_id'] = $message->to_profile;
892 $dmsg['created_at'] = $this->dateTwitter($message->created);
893 $dmsg['sender_screen_name'] = $from_profile->nickname;
894 $dmsg['recipient_screen_name'] = $to_profile->nickname;
895 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
896 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
901 function rssDirectMessageArray($message)
905 $from = $message->getFrom();
907 $entry['title'] = sprintf('Message from %1$s to %2$s',
908 $from->nickname, $message->getTo()->nickname);
910 $entry['content'] = common_xml_safe_str($message->rendered);
911 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
912 $entry['published'] = common_date_iso8601($message->created);
914 $taguribase = TagURI::base();
916 $entry['id'] = "tag:$taguribase:$entry[link]";
917 $entry['updated'] = $entry['published'];
919 $entry['author-name'] = $from->getBestName();
920 $entry['author-uri'] = $from->homepage;
922 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
924 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
925 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
929 $entry['description'] = $entry['content'];
930 $entry['pubDate'] = common_date_rfc2822($message->created);
931 $entry['guid'] = $entry['link'];
936 function showSingleXmlDirectMessage($message)
938 $this->initDocument('xml');
939 $dmsg = $this->directMessageArray($message);
940 $this->showXmlDirectMessage($dmsg);
941 $this->endDocument('xml');
944 function showSingleJsonDirectMessage($message)
946 $this->initDocument('json');
947 $dmsg = $this->directMessageArray($message);
948 $this->showJsonObjects($dmsg);
949 $this->endDocument('json');
952 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
955 $this->initDocument('atom');
957 $this->element('title', null, common_xml_safe_str($title));
958 $this->element('id', null, $id);
959 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
961 if (!is_null($selfuri)) {
962 $this->element('link', array('href' => $selfuri,
963 'rel' => 'self', 'type' => 'application/atom+xml'), null);
966 $this->element('updated', null, common_date_iso8601('now'));
967 $this->element('subtitle', null, common_xml_safe_str($subtitle));
969 if (is_array($group)) {
970 foreach ($group as $g) {
971 $this->raw($g->asAtomEntry());
974 while ($group->fetch()) {
975 $this->raw($group->asAtomEntry());
979 $this->endDocument('atom');
983 function showJsonTimeline($notice)
986 $this->initDocument('json');
990 if (is_array($notice)) {
991 foreach ($notice as $n) {
992 $twitter_status = $this->twitterStatusArray($n);
993 array_push($statuses, $twitter_status);
996 while ($notice->fetch()) {
997 $twitter_status = $this->twitterStatusArray($notice);
998 array_push($statuses, $twitter_status);
1002 $this->showJsonObjects($statuses);
1004 $this->endDocument('json');
1007 function showJsonGroups($group)
1010 $this->initDocument('json');
1014 if (is_array($group)) {
1015 foreach ($group as $g) {
1016 $twitter_group = $this->twitterGroupArray($g);
1017 array_push($groups, $twitter_group);
1020 while ($group->fetch()) {
1021 $twitter_group = $this->twitterGroupArray($group);
1022 array_push($groups, $twitter_group);
1026 $this->showJsonObjects($groups);
1028 $this->endDocument('json');
1031 function showXmlGroups($group)
1034 $this->initDocument('xml');
1035 $this->elementStart('groups', array('type' => 'array'));
1037 if (is_array($group)) {
1038 foreach ($group as $g) {
1039 $twitter_group = $this->twitterGroupArray($g);
1040 $this->showTwitterXmlGroup($twitter_group);
1043 while ($group->fetch()) {
1044 $twitter_group = $this->twitterGroupArray($group);
1045 $this->showTwitterXmlGroup($twitter_group);
1049 $this->elementEnd('groups');
1050 $this->endDocument('xml');
1053 function showTwitterXmlUsers($user)
1056 $this->initDocument('xml');
1057 $this->elementStart('users', array('type' => 'array'));
1059 if (is_array($user)) {
1060 foreach ($user as $u) {
1061 $twitter_user = $this->twitterUserArray($u);
1062 $this->showTwitterXmlUser($twitter_user);
1065 while ($user->fetch()) {
1066 $twitter_user = $this->twitterUserArray($user);
1067 $this->showTwitterXmlUser($twitter_user);
1071 $this->elementEnd('users');
1072 $this->endDocument('xml');
1075 function showJsonUsers($user)
1078 $this->initDocument('json');
1082 if (is_array($user)) {
1083 foreach ($user as $u) {
1084 $twitter_user = $this->twitterUserArray($u);
1085 array_push($users, $twitter_user);
1088 while ($user->fetch()) {
1089 $twitter_user = $this->twitterUserArray($user);
1090 array_push($users, $twitter_user);
1094 $this->showJsonObjects($users);
1096 $this->endDocument('json');
1099 function showSingleJsonGroup($group)
1101 $this->initDocument('json');
1102 $twitter_group = $this->twitterGroupArray($group);
1103 $this->showJsonObjects($twitter_group);
1104 $this->endDocument('json');
1107 function showSingleXmlGroup($group)
1109 $this->initDocument('xml');
1110 $twitter_group = $this->twitterGroupArray($group);
1111 $this->showTwitterXmlGroup($twitter_group);
1112 $this->endDocument('xml');
1115 function dateTwitter($dt)
1117 $dateStr = date('d F Y H:i:s', strtotime($dt));
1118 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1119 $d->setTimezone(new DateTimeZone(common_timezone()));
1120 return $d->format('D M d H:i:s O Y');
1123 function initDocument($type='xml')
1127 header('Content-Type: application/xml; charset=utf-8');
1131 header('Content-Type: application/json; charset=utf-8');
1133 // Check for JSONP callback
1134 $callback = $this->arg('callback');
1136 print $callback . '(';
1140 header("Content-Type: application/rss+xml; charset=utf-8");
1141 $this->initTwitterRss();
1144 header('Content-Type: application/atom+xml; charset=utf-8');
1145 $this->initTwitterAtom();
1148 // TRANS: Client error on an API request with an unsupported data format.
1149 $this->clientError(_('Not a supported data format.'));
1156 function endDocument($type='xml')
1164 // Check for JSONP callback
1165 $callback = $this->arg('callback');
1171 $this->endTwitterRss();
1174 $this->endTwitterRss();
1177 // TRANS: Client error on an API request with an unsupported data format.
1178 $this->clientError(_('Not a supported data format.'));
1184 function clientError($msg, $code = 400, $format = 'xml')
1186 $action = $this->trimmed('action');
1188 common_debug("User error '$code' on '$action': $msg", __FILE__);
1190 if (!array_key_exists($code, ClientErrorAction::$status)) {
1194 $status_string = ClientErrorAction::$status[$code];
1196 header('HTTP/1.1 '.$code.' '.$status_string);
1198 if ($format == 'xml') {
1199 $this->initDocument('xml');
1200 $this->elementStart('hash');
1201 $this->element('error', null, $msg);
1202 $this->element('request', null, $_SERVER['REQUEST_URI']);
1203 $this->elementEnd('hash');
1204 $this->endDocument('xml');
1205 } elseif ($format == 'json'){
1206 $this->initDocument('json');
1207 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1208 print(json_encode($error_array));
1209 $this->endDocument('json');
1212 // If user didn't request a useful format, throw a regular client error
1213 throw new ClientException($msg, $code);
1217 function serverError($msg, $code = 500, $content_type = 'xml')
1219 $action = $this->trimmed('action');
1221 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1223 if (!array_key_exists($code, ServerErrorAction::$status)) {
1227 $status_string = ServerErrorAction::$status[$code];
1229 header('HTTP/1.1 '.$code.' '.$status_string);
1231 if ($content_type == 'xml') {
1232 $this->initDocument('xml');
1233 $this->elementStart('hash');
1234 $this->element('error', null, $msg);
1235 $this->element('request', null, $_SERVER['REQUEST_URI']);
1236 $this->elementEnd('hash');
1237 $this->endDocument('xml');
1239 $this->initDocument('json');
1240 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1241 print(json_encode($error_array));
1242 $this->endDocument('json');
1246 function initTwitterRss()
1249 $this->elementStart(
1253 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1254 'xmlns:georss' => 'http://www.georss.org/georss'
1257 $this->elementStart('channel');
1258 Event::handle('StartApiRss', array($this));
1261 function endTwitterRss()
1263 $this->elementEnd('channel');
1264 $this->elementEnd('rss');
1268 function initTwitterAtom()
1271 // FIXME: don't hardcode the language here!
1272 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1273 'xml:lang' => 'en-US',
1274 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1277 function endTwitterAtom()
1279 $this->elementEnd('feed');
1283 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1285 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1286 switch ($content_type) {
1288 $this->showTwitterXmlUser($profile_array);
1291 $this->showJsonObjects($profile_array);
1294 // TRANS: Client error on an API request with an unsupported data format.
1295 $this->clientError(_('Not a supported data format.'));
1301 function getTargetUser($id)
1305 // Twitter supports these other ways of passing the user ID
1306 if (is_numeric($this->arg('id'))) {
1307 return User::staticGet($this->arg('id'));
1308 } else if ($this->arg('id')) {
1309 $nickname = common_canonical_nickname($this->arg('id'));
1310 return User::staticGet('nickname', $nickname);
1311 } else if ($this->arg('user_id')) {
1312 // This is to ensure that a non-numeric user_id still
1313 // overrides screen_name even if it doesn't get used
1314 if (is_numeric($this->arg('user_id'))) {
1315 return User::staticGet('id', $this->arg('user_id'));
1317 } else if ($this->arg('screen_name')) {
1318 $nickname = common_canonical_nickname($this->arg('screen_name'));
1319 return User::staticGet('nickname', $nickname);
1321 // Fall back to trying the currently authenticated user
1322 return $this->auth_user;
1325 } else if (is_numeric($id)) {
1326 return User::staticGet($id);
1328 $nickname = common_canonical_nickname($id);
1329 return User::staticGet('nickname', $nickname);
1333 function getTargetGroup($id)
1336 if (is_numeric($this->arg('id'))) {
1337 return User_group::staticGet($this->arg('id'));
1338 } else if ($this->arg('id')) {
1339 $nickname = common_canonical_nickname($this->arg('id'));
1340 $local = Local_group::staticGet('nickname', $nickname);
1341 if (empty($local)) {
1344 return User_group::staticGet('id', $local->id);
1346 } else if ($this->arg('group_id')) {
1347 // This is to ensure that a non-numeric user_id still
1348 // overrides screen_name even if it doesn't get used
1349 if (is_numeric($this->arg('group_id'))) {
1350 return User_group::staticGet('id', $this->arg('group_id'));
1352 } else if ($this->arg('group_name')) {
1353 $nickname = common_canonical_nickname($this->arg('group_name'));
1354 $local = Local_group::staticGet('nickname', $nickname);
1355 if (empty($local)) {
1358 return User_group::staticGet('id', $local->group_id);
1362 } else if (is_numeric($id)) {
1363 return User_group::staticGet($id);
1365 $nickname = common_canonical_nickname($id);
1366 $local = Local_group::staticGet('nickname', $nickname);
1367 if (empty($local)) {
1370 return User_group::staticGet('id', $local->group_id);
1376 * Returns query argument or default value if not found. Certain
1377 * parameters used throughout the API are lightly scrubbed and
1378 * bounds checked. This overrides Action::arg().
1380 * @param string $key requested argument
1381 * @param string $def default value to return if $key is not provided
1385 function arg($key, $def=null)
1388 // XXX: Do even more input validation/scrubbing?
1390 if (array_key_exists($key, $this->args)) {
1393 $page = (int)$this->args['page'];
1394 return ($page < 1) ? 1 : $page;
1396 $count = (int)$this->args['count'];
1399 } elseif ($count > 200) {
1405 $since_id = (int)$this->args['since_id'];
1406 return ($since_id < 1) ? 0 : $since_id;
1408 $max_id = (int)$this->args['max_id'];
1409 return ($max_id < 1) ? 0 : $max_id;
1411 return parent::arg($key, $def);
1419 * Calculate the complete URI that called up this action. Used for
1420 * Atom rel="self" links. Warning: this is funky.
1422 * @return string URL a URL suitable for rel="self" Atom links
1424 function getSelfUri()
1426 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1428 $id = $this->arg('id');
1429 $aargs = array('format' => $this->format);
1434 $tag = $this->arg('tag');
1436 $aargs['tag'] = $tag;
1439 parse_str($_SERVER['QUERY_STRING'], $params);
1441 if (!empty($params)) {
1442 unset($params['p']);
1443 $pstring = http_build_query($params);
1446 $uri = common_local_url($action, $aargs);
1448 if (!empty($pstring)) {
1449 $uri .= '?' . $pstring;