3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009 StatusNet, Inc.
31 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
32 * @link http://status.net/
35 /* External API usage documentation. Please update when you change how the API works. */
37 /*! @mainpage StatusNet REST API
41 Some explanatory text about the API would be nice.
45 @subsection timelinesmethods_sec Timeline Methods
47 @li @ref publictimeline
48 @li @ref friendstimeline
50 @subsection statusmethods_sec Status Methods
52 @li @ref statusesupdate
54 @subsection usermethods_sec User Methods
56 @subsection directmessagemethods_sec Direct Message Methods
58 @subsection friendshipmethods_sec Friendship Methods
60 @subsection socialgraphmethods_sec Social Graph Methods
62 @subsection accountmethods_sec Account Methods
64 @subsection favoritesmethods_sec Favorites Methods
66 @subsection blockmethods_sec Block Methods
68 @subsection oauthmethods_sec OAuth Methods
70 @subsection helpmethods_sec Help Methods
72 @subsection groupmethods_sec Group Methods
74 @page apiroot API Root
76 The URLs for methods referred to in this API documentation are
77 relative to the StatusNet API root. The API root is determined by the
78 site's @b server and @b path variables, which are generally specified
79 in config.php. For example:
82 $config['site']['server'] = 'example.org';
83 $config['site']['path'] = 'statusnet'
86 The pattern for a site's API root is: @c protocol://server/path/api E.g:
88 @c http://example.org/statusnet/api
90 The @b path can be empty. In that case the API root would simply be:
92 @c http://example.org/api
96 if (!defined('STATUSNET')) {
101 * Contains most of the Twitter-compatible API output functions.
105 * @author Craig Andrews <candrews@integralblue.com>
106 * @author Dan Moore <dan@moore.cx>
107 * @author Evan Prodromou <evan@status.net>
108 * @author Jeffery To <jeffery.to@gmail.com>
109 * @author Toby Inkster <mail@tobyinkster.co.uk>
110 * @author Zach Copley <zach@status.net>
111 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
112 * @link http://status.net/
115 class ApiAction extends Action
118 const READ_WRITE = 2;
122 var $auth_user = null;
126 var $since_id = null;
128 var $access = self::READ_ONLY; // read (default) or read-write
133 * @param array $args Web and URL arguments
135 * @return boolean false if user doesn't exist
138 function prepare($args)
140 StatusNet::setApi(true); // reduce exception reports to aid in debugging
141 parent::prepare($args);
143 $this->format = $this->arg('format');
144 $this->page = (int)$this->arg('page', 1);
145 $this->count = (int)$this->arg('count', 20);
146 $this->max_id = (int)$this->arg('max_id', 0);
147 $this->since_id = (int)$this->arg('since_id', 0);
149 if ($this->arg('since')) {
150 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
159 * @param array $args Arguments from $_REQUEST
164 function handle($args)
166 header('Access-Control-Allow-Origin: *');
167 parent::handle($args);
171 * Overrides XMLOutputter::element to write booleans as strings (true|false).
172 * See that method's documentation for more info.
174 * @param string $tag Element type or tagname
175 * @param array $attrs Array of element attributes, as
177 * @param string $content string content of the element
181 function element($tag, $attrs=null, $content=null)
183 if (is_bool($content)) {
184 $content = ($content ? 'true' : 'false');
187 return parent::element($tag, $attrs, $content);
190 function twitterUserArray($profile, $get_notice=false)
192 $twitter_user = array();
194 $twitter_user['id'] = intval($profile->id);
195 $twitter_user['name'] = $profile->getBestName();
196 $twitter_user['screen_name'] = $profile->nickname;
197 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
198 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
200 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
201 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
202 Avatar::defaultImage(AVATAR_STREAM_SIZE);
204 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
205 $twitter_user['protected'] = false; # not supported by StatusNet yet
206 $twitter_user['followers_count'] = $profile->subscriberCount();
209 $user = $profile->getUser();
211 // Note: some profiles don't have an associated user
213 $defaultDesign = Design::siteDesign();
216 $design = $user->getDesign();
219 if (empty($design)) {
220 $design = $defaultDesign;
223 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
224 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
225 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
226 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
227 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
228 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
229 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
230 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
231 $twitter_user['profile_sidebar_border_color'] = '';
233 $twitter_user['friends_count'] = $profile->subscriptionCount();
235 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
237 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
241 if (!empty($user) && $user->timezone) {
242 $timezone = $user->timezone;
246 $t->setTimezone(new DateTimeZone($timezone));
248 $twitter_user['utc_offset'] = $t->format('Z');
249 $twitter_user['time_zone'] = $timezone;
251 $twitter_user['profile_background_image_url']
252 = empty($design->backgroundimage)
253 ? '' : ($design->disposition & BACKGROUND_ON)
254 ? Design::url($design->backgroundimage) : '';
256 $twitter_user['profile_background_tile']
257 = empty($design->disposition)
258 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
260 $twitter_user['statuses_count'] = $profile->noticeCount();
262 // Is the requesting user following this user?
263 $twitter_user['following'] = false;
264 $twitter_user['notifications'] = false;
266 if (isset($this->auth_user)) {
268 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
271 $sub = Subscription::pkeyGet(array('subscriber' =>
272 $this->auth_user->id,
273 'subscribed' => $profile->id));
276 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
281 $notice = $profile->getCurrentNotice();
284 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
288 return $twitter_user;
291 function twitterStatusArray($notice, $include_user=true)
293 $base = $this->twitterSimpleStatusArray($notice, $include_user);
295 if (!empty($notice->repeat_of)) {
296 $original = Notice::staticGet('id', $notice->repeat_of);
297 if (!empty($original)) {
298 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
299 $base['retweeted_status'] = $original_array;
306 function twitterSimpleStatusArray($notice, $include_user=true)
308 $profile = $notice->getProfile();
310 $twitter_status = array();
311 $twitter_status['text'] = $notice->content;
312 $twitter_status['truncated'] = false; # Not possible on StatusNet
313 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
314 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
315 intval($notice->reply_to) : null;
319 $ns = $notice->getSource();
321 if (!empty($ns->name) && !empty($ns->url)) {
322 $source = '<a href="' . $ns->url . '" rel="nofollow">' . $ns->name . '</a>';
328 $twitter_status['source'] = htmlentities($source);
329 $twitter_status['id'] = intval($notice->id);
331 $replier_profile = null;
333 if ($notice->reply_to) {
334 $reply = Notice::staticGet(intval($notice->reply_to));
336 $replier_profile = $reply->getProfile();
340 $twitter_status['in_reply_to_user_id'] =
341 ($replier_profile) ? intval($replier_profile->id) : null;
342 $twitter_status['in_reply_to_screen_name'] =
343 ($replier_profile) ? $replier_profile->nickname : null;
345 if (isset($notice->lat) && isset($notice->lon)) {
346 // This is the format that GeoJSON expects stuff to be in
347 $twitter_status['geo'] = array('type' => 'Point',
348 'coordinates' => array((float) $notice->lat,
349 (float) $notice->lon));
351 $twitter_status['geo'] = null;
354 if (isset($this->auth_user)) {
355 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
357 $twitter_status['favorited'] = false;
361 $attachments = $notice->attachments();
363 if (!empty($attachments)) {
365 $twitter_status['attachments'] = array();
367 foreach ($attachments as $attachment) {
368 $enclosure_o=$attachment->getEnclosure();
370 $enclosure = array();
371 $enclosure['url'] = $enclosure_o->url;
372 $enclosure['mimetype'] = $enclosure_o->mimetype;
373 $enclosure['size'] = $enclosure_o->size;
374 $twitter_status['attachments'][] = $enclosure;
379 if ($include_user && $profile) {
380 # Don't get notice (recursive!)
381 $twitter_user = $this->twitterUserArray($profile, false);
382 $twitter_status['user'] = $twitter_user;
385 return $twitter_status;
388 function twitterGroupArray($group)
390 $twitter_group=array();
391 $twitter_group['id']=$group->id;
392 $twitter_group['url']=$group->permalink();
393 $twitter_group['nickname']=$group->nickname;
394 $twitter_group['fullname']=$group->fullname;
395 $twitter_group['original_logo']=$group->original_logo;
396 $twitter_group['homepage_logo']=$group->homepage_logo;
397 $twitter_group['stream_logo']=$group->stream_logo;
398 $twitter_group['mini_logo']=$group->mini_logo;
399 $twitter_group['homepage']=$group->homepage;
400 $twitter_group['description']=$group->description;
401 $twitter_group['location']=$group->location;
402 $twitter_group['created']=$this->dateTwitter($group->created);
403 $twitter_group['modified']=$this->dateTwitter($group->modified);
404 return $twitter_group;
407 function twitterRssGroupArray($group)
410 $entry['content']=$group->description;
411 $entry['title']=$group->nickname;
412 $entry['link']=$group->permalink();
413 $entry['published']=common_date_iso8601($group->created);
414 $entry['updated']==common_date_iso8601($group->modified);
415 $taguribase = common_config('integration', 'groupuri');
416 $entry['id'] = "group:$groupuribase:$entry[link]";
418 $entry['description'] = $entry['content'];
419 $entry['pubDate'] = common_date_rfc2822($group->created);
420 $entry['guid'] = $entry['link'];
425 function twitterRssEntryArray($notice)
427 $profile = $notice->getProfile();
430 // We trim() to avoid extraneous whitespace in the output
432 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
433 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
434 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
435 $entry['published'] = common_date_iso8601($notice->created);
437 $taguribase = TagURI::base();
438 $entry['id'] = "tag:$taguribase:$entry[link]";
440 $entry['updated'] = $entry['published'];
441 $entry['author'] = $profile->getBestName();
444 $attachments = $notice->attachments();
445 $enclosures = array();
447 foreach ($attachments as $attachment) {
448 $enclosure_o=$attachment->getEnclosure();
450 $enclosure = array();
451 $enclosure['url'] = $enclosure_o->url;
452 $enclosure['mimetype'] = $enclosure_o->mimetype;
453 $enclosure['size'] = $enclosure_o->size;
454 $enclosures[] = $enclosure;
458 if (!empty($enclosures)) {
459 $entry['enclosures'] = $enclosures;
463 $tag = new Notice_tag();
464 $tag->notice_id = $notice->id;
466 $entry['tags']=array();
467 while ($tag->fetch()) {
468 $entry['tags'][]=$tag->tag;
474 $entry['description'] = $entry['content'];
475 $entry['pubDate'] = common_date_rfc2822($notice->created);
476 $entry['guid'] = $entry['link'];
478 if (isset($notice->lat) && isset($notice->lon)) {
479 // This is the format that GeoJSON expects stuff to be in.
480 // showGeoRSS() below uses it for XML output, so we reuse it
481 $entry['geo'] = array('type' => 'Point',
482 'coordinates' => array((float) $notice->lat,
483 (float) $notice->lon));
485 $entry['geo'] = null;
491 function twitterRelationshipArray($source, $target)
493 $relationship = array();
495 $relationship['source'] =
496 $this->relationshipDetailsArray($source, $target);
497 $relationship['target'] =
498 $this->relationshipDetailsArray($target, $source);
500 return array('relationship' => $relationship);
503 function relationshipDetailsArray($source, $target)
507 $details['screen_name'] = $source->nickname;
508 $details['followed_by'] = $target->isSubscribed($source);
509 $details['following'] = $source->isSubscribed($target);
511 $notifications = false;
513 if ($source->isSubscribed($target)) {
515 $sub = Subscription::pkeyGet(array('subscriber' =>
516 $source->id, 'subscribed' => $target->id));
519 $notifications = ($sub->jabber || $sub->sms);
523 $details['notifications_enabled'] = $notifications;
524 $details['blocking'] = $source->hasBlocked($target);
525 $details['id'] = $source->id;
530 function showTwitterXmlRelationship($relationship)
532 $this->elementStart('relationship');
534 foreach($relationship as $element => $value) {
535 if ($element == 'source' || $element == 'target') {
536 $this->elementStart($element);
537 $this->showXmlRelationshipDetails($value);
538 $this->elementEnd($element);
542 $this->elementEnd('relationship');
545 function showXmlRelationshipDetails($details)
547 foreach($details as $element => $value) {
548 $this->element($element, null, $value);
552 function showTwitterXmlStatus($twitter_status, $tag='status')
554 $this->elementStart($tag);
555 foreach($twitter_status as $element => $value) {
558 $this->showTwitterXmlUser($twitter_status['user']);
561 $this->element($element, null, common_xml_safe_str($value));
564 $this->showXmlAttachments($twitter_status['attachments']);
567 $this->showGeoXML($value);
569 case 'retweeted_status':
570 $this->showTwitterXmlStatus($value, 'retweeted_status');
573 $this->element($element, null, $value);
576 $this->elementEnd($tag);
579 function showTwitterXmlGroup($twitter_group)
581 $this->elementStart('group');
582 foreach($twitter_group as $element => $value) {
583 $this->element($element, null, $value);
585 $this->elementEnd('group');
588 function showTwitterXmlUser($twitter_user, $role='user')
590 $this->elementStart($role);
591 foreach($twitter_user as $element => $value) {
592 if ($element == 'status') {
593 $this->showTwitterXmlStatus($twitter_user['status']);
595 $this->element($element, null, $value);
598 $this->elementEnd($role);
601 function showXmlAttachments($attachments) {
602 if (!empty($attachments)) {
603 $this->elementStart('attachments', array('type' => 'array'));
604 foreach ($attachments as $attachment) {
606 $attrs['url'] = $attachment['url'];
607 $attrs['mimetype'] = $attachment['mimetype'];
608 $attrs['size'] = $attachment['size'];
609 $this->element('enclosure', $attrs, '');
611 $this->elementEnd('attachments');
615 function showGeoXML($geo)
619 $this->element('geo');
621 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
622 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
623 $this->elementEnd('geo');
627 function showGeoRSS($geo)
633 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
638 function showTwitterRssItem($entry)
640 $this->elementStart('item');
641 $this->element('title', null, $entry['title']);
642 $this->element('description', null, $entry['description']);
643 $this->element('pubDate', null, $entry['pubDate']);
644 $this->element('guid', null, $entry['guid']);
645 $this->element('link', null, $entry['link']);
647 # RSS only supports 1 enclosure per item
648 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
649 $enclosure = $entry['enclosures'][0];
650 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
653 if(array_key_exists('tags', $entry)){
654 foreach($entry['tags'] as $tag){
655 $this->element('category', null,$tag);
659 $this->showGeoRSS($entry['geo']);
660 $this->elementEnd('item');
663 function showJsonObjects($objects)
665 print(json_encode($objects));
668 function showSingleXmlStatus($notice)
670 $this->initDocument('xml');
671 $twitter_status = $this->twitterStatusArray($notice);
672 $this->showTwitterXmlStatus($twitter_status);
673 $this->endDocument('xml');
676 function show_single_json_status($notice)
678 $this->initDocument('json');
679 $status = $this->twitterStatusArray($notice);
680 $this->showJsonObjects($status);
681 $this->endDocument('json');
684 function showXmlTimeline($notice)
687 $this->initDocument('xml');
688 $this->elementStart('statuses', array('type' => 'array'));
690 if (is_array($notice)) {
691 foreach ($notice as $n) {
692 $twitter_status = $this->twitterStatusArray($n);
693 $this->showTwitterXmlStatus($twitter_status);
696 while ($notice->fetch()) {
697 $twitter_status = $this->twitterStatusArray($notice);
698 $this->showTwitterXmlStatus($twitter_status);
702 $this->elementEnd('statuses');
703 $this->endDocument('xml');
706 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
709 $this->initDocument('rss');
711 $this->element('title', null, $title);
712 $this->element('link', null, $link);
714 if (!is_null($self)) {
718 'type' => 'application/rss+xml',
725 if (!is_null($suplink)) {
726 // For FriendFeed's SUP protocol
727 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
728 'rel' => 'http://api.friendfeed.com/2008/03#sup',
730 'type' => 'application/json'));
733 if (!is_null($logo)) {
734 $this->elementStart('image');
735 $this->element('link', null, $link);
736 $this->element('title', null, $title);
737 $this->element('url', null, $logo);
738 $this->elementEnd('image');
741 $this->element('description', null, $subtitle);
742 $this->element('language', null, 'en-us');
743 $this->element('ttl', null, '40');
745 if (is_array($notice)) {
746 foreach ($notice as $n) {
747 $entry = $this->twitterRssEntryArray($n);
748 $this->showTwitterRssItem($entry);
751 while ($notice->fetch()) {
752 $entry = $this->twitterRssEntryArray($notice);
753 $this->showTwitterRssItem($entry);
757 $this->endTwitterRss();
760 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
763 $this->initDocument('atom');
765 $this->element('title', null, $title);
766 $this->element('id', null, $id);
767 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
769 if (!is_null($logo)) {
770 $this->element('logo',null,$logo);
773 if (!is_null($suplink)) {
774 # For FriendFeed's SUP protocol
775 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
777 'type' => 'application/json'));
780 if (!is_null($selfuri)) {
781 $this->element('link', array('href' => $selfuri,
782 'rel' => 'self', 'type' => 'application/atom+xml'), null);
785 $this->element('updated', null, common_date_iso8601('now'));
786 $this->element('subtitle', null, $subtitle);
788 if (is_array($notice)) {
789 foreach ($notice as $n) {
790 $this->raw($n->asAtomEntry());
793 while ($notice->fetch()) {
794 $this->raw($notice->asAtomEntry());
798 $this->endDocument('atom');
802 function showRssGroups($group, $title, $link, $subtitle)
805 $this->initDocument('rss');
807 $this->element('title', null, $title);
808 $this->element('link', null, $link);
809 $this->element('description', null, $subtitle);
810 $this->element('language', null, 'en-us');
811 $this->element('ttl', null, '40');
813 if (is_array($group)) {
814 foreach ($group as $g) {
815 $twitter_group = $this->twitterRssGroupArray($g);
816 $this->showTwitterRssItem($twitter_group);
819 while ($group->fetch()) {
820 $twitter_group = $this->twitterRssGroupArray($group);
821 $this->showTwitterRssItem($twitter_group);
825 $this->endTwitterRss();
828 function showTwitterAtomEntry($entry)
830 $this->elementStart('entry');
831 $this->element('title', null, common_xml_safe_str($entry['title']));
834 array('type' => 'html'),
835 common_xml_safe_str($entry['content'])
837 $this->element('id', null, $entry['id']);
838 $this->element('published', null, $entry['published']);
839 $this->element('updated', null, $entry['updated']);
840 $this->element('link', array('type' => 'text/html',
841 'href' => $entry['link'],
842 'rel' => 'alternate'));
843 $this->element('link', array('type' => $entry['avatar-type'],
844 'href' => $entry['avatar'],
846 $this->elementStart('author');
848 $this->element('name', null, $entry['author-name']);
849 $this->element('uri', null, $entry['author-uri']);
851 $this->elementEnd('author');
852 $this->elementEnd('entry');
855 function showXmlDirectMessage($dm)
857 $this->elementStart('direct_message');
858 foreach($dm as $element => $value) {
862 $this->showTwitterXmlUser($value, $element);
865 $this->element($element, null, common_xml_safe_str($value));
868 $this->element($element, null, $value);
872 $this->elementEnd('direct_message');
875 function directMessageArray($message)
879 $from_profile = $message->getFrom();
880 $to_profile = $message->getTo();
882 $dmsg['id'] = $message->id;
883 $dmsg['sender_id'] = $message->from_profile;
884 $dmsg['text'] = trim($message->content);
885 $dmsg['recipient_id'] = $message->to_profile;
886 $dmsg['created_at'] = $this->dateTwitter($message->created);
887 $dmsg['sender_screen_name'] = $from_profile->nickname;
888 $dmsg['recipient_screen_name'] = $to_profile->nickname;
889 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
890 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
895 function rssDirectMessageArray($message)
899 $from = $message->getFrom();
901 $entry['title'] = sprintf('Message from %1$s to %2$s',
902 $from->nickname, $message->getTo()->nickname);
904 $entry['content'] = common_xml_safe_str($message->rendered);
905 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
906 $entry['published'] = common_date_iso8601($message->created);
908 $taguribase = TagURI::base();
910 $entry['id'] = "tag:$taguribase:$entry[link]";
911 $entry['updated'] = $entry['published'];
913 $entry['author-name'] = $from->getBestName();
914 $entry['author-uri'] = $from->homepage;
916 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
918 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
919 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
923 $entry['description'] = $entry['content'];
924 $entry['pubDate'] = common_date_rfc2822($message->created);
925 $entry['guid'] = $entry['link'];
930 function showSingleXmlDirectMessage($message)
932 $this->initDocument('xml');
933 $dmsg = $this->directMessageArray($message);
934 $this->showXmlDirectMessage($dmsg);
935 $this->endDocument('xml');
938 function showSingleJsonDirectMessage($message)
940 $this->initDocument('json');
941 $dmsg = $this->directMessageArray($message);
942 $this->showJsonObjects($dmsg);
943 $this->endDocument('json');
946 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
949 $this->initDocument('atom');
951 $this->element('title', null, common_xml_safe_str($title));
952 $this->element('id', null, $id);
953 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
955 if (!is_null($selfuri)) {
956 $this->element('link', array('href' => $selfuri,
957 'rel' => 'self', 'type' => 'application/atom+xml'), null);
960 $this->element('updated', null, common_date_iso8601('now'));
961 $this->element('subtitle', null, common_xml_safe_str($subtitle));
963 if (is_array($group)) {
964 foreach ($group as $g) {
965 $this->raw($g->asAtomEntry());
968 while ($group->fetch()) {
969 $this->raw($group->asAtomEntry());
973 $this->endDocument('atom');
977 function showJsonTimeline($notice)
980 $this->initDocument('json');
984 if (is_array($notice)) {
985 foreach ($notice as $n) {
986 $twitter_status = $this->twitterStatusArray($n);
987 array_push($statuses, $twitter_status);
990 while ($notice->fetch()) {
991 $twitter_status = $this->twitterStatusArray($notice);
992 array_push($statuses, $twitter_status);
996 $this->showJsonObjects($statuses);
998 $this->endDocument('json');
1001 function showJsonGroups($group)
1004 $this->initDocument('json');
1008 if (is_array($group)) {
1009 foreach ($group as $g) {
1010 $twitter_group = $this->twitterGroupArray($g);
1011 array_push($groups, $twitter_group);
1014 while ($group->fetch()) {
1015 $twitter_group = $this->twitterGroupArray($group);
1016 array_push($groups, $twitter_group);
1020 $this->showJsonObjects($groups);
1022 $this->endDocument('json');
1025 function showXmlGroups($group)
1028 $this->initDocument('xml');
1029 $this->elementStart('groups', array('type' => 'array'));
1031 if (is_array($group)) {
1032 foreach ($group as $g) {
1033 $twitter_group = $this->twitterGroupArray($g);
1034 $this->showTwitterXmlGroup($twitter_group);
1037 while ($group->fetch()) {
1038 $twitter_group = $this->twitterGroupArray($group);
1039 $this->showTwitterXmlGroup($twitter_group);
1043 $this->elementEnd('groups');
1044 $this->endDocument('xml');
1047 function showTwitterXmlUsers($user)
1050 $this->initDocument('xml');
1051 $this->elementStart('users', array('type' => 'array'));
1053 if (is_array($user)) {
1054 foreach ($user as $u) {
1055 $twitter_user = $this->twitterUserArray($u);
1056 $this->showTwitterXmlUser($twitter_user);
1059 while ($user->fetch()) {
1060 $twitter_user = $this->twitterUserArray($user);
1061 $this->showTwitterXmlUser($twitter_user);
1065 $this->elementEnd('users');
1066 $this->endDocument('xml');
1069 function showJsonUsers($user)
1072 $this->initDocument('json');
1076 if (is_array($user)) {
1077 foreach ($user as $u) {
1078 $twitter_user = $this->twitterUserArray($u);
1079 array_push($users, $twitter_user);
1082 while ($user->fetch()) {
1083 $twitter_user = $this->twitterUserArray($user);
1084 array_push($users, $twitter_user);
1088 $this->showJsonObjects($users);
1090 $this->endDocument('json');
1093 function showSingleJsonGroup($group)
1095 $this->initDocument('json');
1096 $twitter_group = $this->twitterGroupArray($group);
1097 $this->showJsonObjects($twitter_group);
1098 $this->endDocument('json');
1101 function showSingleXmlGroup($group)
1103 $this->initDocument('xml');
1104 $twitter_group = $this->twitterGroupArray($group);
1105 $this->showTwitterXmlGroup($twitter_group);
1106 $this->endDocument('xml');
1109 function dateTwitter($dt)
1111 $dateStr = date('d F Y H:i:s', strtotime($dt));
1112 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1113 $d->setTimezone(new DateTimeZone(common_timezone()));
1114 return $d->format('D M d H:i:s O Y');
1117 function initDocument($type='xml')
1121 header('Content-Type: application/xml; charset=utf-8');
1125 header('Content-Type: application/json; charset=utf-8');
1127 // Check for JSONP callback
1128 $callback = $this->arg('callback');
1130 print $callback . '(';
1134 header("Content-Type: application/rss+xml; charset=utf-8");
1135 $this->initTwitterRss();
1138 header('Content-Type: application/atom+xml; charset=utf-8');
1139 $this->initTwitterAtom();
1142 // TRANS: Client error on an API request with an unsupported data format.
1143 $this->clientError(_('Not a supported data format.'));
1150 function endDocument($type='xml')
1158 // Check for JSONP callback
1159 $callback = $this->arg('callback');
1165 $this->endTwitterRss();
1168 $this->endTwitterRss();
1171 // TRANS: Client error on an API request with an unsupported data format.
1172 $this->clientError(_('Not a supported data format.'));
1178 function clientError($msg, $code = 400, $format = 'xml')
1180 $action = $this->trimmed('action');
1182 common_debug("User error '$code' on '$action': $msg", __FILE__);
1184 if (!array_key_exists($code, ClientErrorAction::$status)) {
1188 $status_string = ClientErrorAction::$status[$code];
1190 header('HTTP/1.1 '.$code.' '.$status_string);
1192 if ($format == 'xml') {
1193 $this->initDocument('xml');
1194 $this->elementStart('hash');
1195 $this->element('error', null, $msg);
1196 $this->element('request', null, $_SERVER['REQUEST_URI']);
1197 $this->elementEnd('hash');
1198 $this->endDocument('xml');
1199 } elseif ($format == 'json'){
1200 $this->initDocument('json');
1201 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1202 print(json_encode($error_array));
1203 $this->endDocument('json');
1206 // If user didn't request a useful format, throw a regular client error
1207 throw new ClientException($msg, $code);
1211 function serverError($msg, $code = 500, $content_type = 'xml')
1213 $action = $this->trimmed('action');
1215 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1217 if (!array_key_exists($code, ServerErrorAction::$status)) {
1221 $status_string = ServerErrorAction::$status[$code];
1223 header('HTTP/1.1 '.$code.' '.$status_string);
1225 if ($content_type == 'xml') {
1226 $this->initDocument('xml');
1227 $this->elementStart('hash');
1228 $this->element('error', null, $msg);
1229 $this->element('request', null, $_SERVER['REQUEST_URI']);
1230 $this->elementEnd('hash');
1231 $this->endDocument('xml');
1233 $this->initDocument('json');
1234 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1235 print(json_encode($error_array));
1236 $this->endDocument('json');
1240 function initTwitterRss()
1243 $this->elementStart(
1247 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1248 'xmlns:georss' => 'http://www.georss.org/georss'
1251 $this->elementStart('channel');
1252 Event::handle('StartApiRss', array($this));
1255 function endTwitterRss()
1257 $this->elementEnd('channel');
1258 $this->elementEnd('rss');
1262 function initTwitterAtom()
1265 // FIXME: don't hardcode the language here!
1266 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1267 'xml:lang' => 'en-US',
1268 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1271 function endTwitterAtom()
1273 $this->elementEnd('feed');
1277 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1279 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1280 switch ($content_type) {
1282 $this->showTwitterXmlUser($profile_array);
1285 $this->showJsonObjects($profile_array);
1288 // TRANS: Client error on an API request with an unsupported data format.
1289 $this->clientError(_('Not a supported data format.'));
1295 function getTargetUser($id)
1299 // Twitter supports these other ways of passing the user ID
1300 if (is_numeric($this->arg('id'))) {
1301 return User::staticGet($this->arg('id'));
1302 } else if ($this->arg('id')) {
1303 $nickname = common_canonical_nickname($this->arg('id'));
1304 return User::staticGet('nickname', $nickname);
1305 } else if ($this->arg('user_id')) {
1306 // This is to ensure that a non-numeric user_id still
1307 // overrides screen_name even if it doesn't get used
1308 if (is_numeric($this->arg('user_id'))) {
1309 return User::staticGet('id', $this->arg('user_id'));
1311 } else if ($this->arg('screen_name')) {
1312 $nickname = common_canonical_nickname($this->arg('screen_name'));
1313 return User::staticGet('nickname', $nickname);
1315 // Fall back to trying the currently authenticated user
1316 return $this->auth_user;
1319 } else if (is_numeric($id)) {
1320 return User::staticGet($id);
1322 $nickname = common_canonical_nickname($id);
1323 return User::staticGet('nickname', $nickname);
1327 function getTargetGroup($id)
1330 if (is_numeric($this->arg('id'))) {
1331 return User_group::staticGet($this->arg('id'));
1332 } else if ($this->arg('id')) {
1333 $nickname = common_canonical_nickname($this->arg('id'));
1334 $local = Local_group::staticGet('nickname', $nickname);
1335 if (empty($local)) {
1338 return User_group::staticGet('id', $local->id);
1340 } else if ($this->arg('group_id')) {
1341 // This is to ensure that a non-numeric user_id still
1342 // overrides screen_name even if it doesn't get used
1343 if (is_numeric($this->arg('group_id'))) {
1344 return User_group::staticGet('id', $this->arg('group_id'));
1346 } else if ($this->arg('group_name')) {
1347 $nickname = common_canonical_nickname($this->arg('group_name'));
1348 $local = Local_group::staticGet('nickname', $nickname);
1349 if (empty($local)) {
1352 return User_group::staticGet('id', $local->group_id);
1356 } else if (is_numeric($id)) {
1357 return User_group::staticGet($id);
1359 $nickname = common_canonical_nickname($id);
1360 $local = Local_group::staticGet('nickname', $nickname);
1361 if (empty($local)) {
1364 return User_group::staticGet('id', $local->group_id);
1370 * Returns query argument or default value if not found. Certain
1371 * parameters used throughout the API are lightly scrubbed and
1372 * bounds checked. This overrides Action::arg().
1374 * @param string $key requested argument
1375 * @param string $def default value to return if $key is not provided
1379 function arg($key, $def=null)
1382 // XXX: Do even more input validation/scrubbing?
1384 if (array_key_exists($key, $this->args)) {
1387 $page = (int)$this->args['page'];
1388 return ($page < 1) ? 1 : $page;
1390 $count = (int)$this->args['count'];
1393 } elseif ($count > 200) {
1399 $since_id = (int)$this->args['since_id'];
1400 return ($since_id < 1) ? 0 : $since_id;
1402 $max_id = (int)$this->args['max_id'];
1403 return ($max_id < 1) ? 0 : $max_id;
1405 return parent::arg($key, $def);
1413 * Calculate the complete URI that called up this action. Used for
1414 * Atom rel="self" links. Warning: this is funky.
1416 * @return string URL a URL suitable for rel="self" Atom links
1418 function getSelfUri()
1420 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1422 $id = $this->arg('id');
1423 $aargs = array('format' => $this->format);
1428 $tag = $this->arg('tag');
1430 $aargs['tag'] = $tag;
1433 parse_str($_SERVER['QUERY_STRING'], $params);
1435 if (!empty($params)) {
1436 unset($params['p']);
1437 $pstring = http_build_query($params);
1440 $uri = common_local_url($action, $aargs);
1442 if (!empty($pstring)) {
1443 $uri .= '?' . $pstring;