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;
316 $twitter_status['source'] = $this->sourceLink($notice->source);
317 $twitter_status['id'] = intval($notice->id);
319 $replier_profile = null;
321 if ($notice->reply_to) {
322 $reply = Notice::staticGet(intval($notice->reply_to));
324 $replier_profile = $reply->getProfile();
328 $twitter_status['in_reply_to_user_id'] =
329 ($replier_profile) ? intval($replier_profile->id) : null;
330 $twitter_status['in_reply_to_screen_name'] =
331 ($replier_profile) ? $replier_profile->nickname : null;
333 if (isset($notice->lat) && isset($notice->lon)) {
334 // This is the format that GeoJSON expects stuff to be in
335 $twitter_status['geo'] = array('type' => 'Point',
336 'coordinates' => array((float) $notice->lat,
337 (float) $notice->lon));
339 $twitter_status['geo'] = null;
342 if (isset($this->auth_user)) {
343 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
345 $twitter_status['favorited'] = false;
349 $attachments = $notice->attachments();
351 if (!empty($attachments)) {
353 $twitter_status['attachments'] = array();
355 foreach ($attachments as $attachment) {
356 $enclosure_o=$attachment->getEnclosure();
358 $enclosure = array();
359 $enclosure['url'] = $enclosure_o->url;
360 $enclosure['mimetype'] = $enclosure_o->mimetype;
361 $enclosure['size'] = $enclosure_o->size;
362 $twitter_status['attachments'][] = $enclosure;
367 if ($include_user && $profile) {
368 # Don't get notice (recursive!)
369 $twitter_user = $this->twitterUserArray($profile, false);
370 $twitter_status['user'] = $twitter_user;
373 return $twitter_status;
376 function twitterGroupArray($group)
378 $twitter_group=array();
379 $twitter_group['id']=$group->id;
380 $twitter_group['url']=$group->permalink();
381 $twitter_group['nickname']=$group->nickname;
382 $twitter_group['fullname']=$group->fullname;
383 $twitter_group['original_logo']=$group->original_logo;
384 $twitter_group['homepage_logo']=$group->homepage_logo;
385 $twitter_group['stream_logo']=$group->stream_logo;
386 $twitter_group['mini_logo']=$group->mini_logo;
387 $twitter_group['homepage']=$group->homepage;
388 $twitter_group['description']=$group->description;
389 $twitter_group['location']=$group->location;
390 $twitter_group['created']=$this->dateTwitter($group->created);
391 $twitter_group['modified']=$this->dateTwitter($group->modified);
392 return $twitter_group;
395 function twitterRssGroupArray($group)
398 $entry['content']=$group->description;
399 $entry['title']=$group->nickname;
400 $entry['link']=$group->permalink();
401 $entry['published']=common_date_iso8601($group->created);
402 $entry['updated']==common_date_iso8601($group->modified);
403 $taguribase = common_config('integration', 'groupuri');
404 $entry['id'] = "group:$groupuribase:$entry[link]";
406 $entry['description'] = $entry['content'];
407 $entry['pubDate'] = common_date_rfc2822($group->created);
408 $entry['guid'] = $entry['link'];
413 function twitterRssEntryArray($notice)
415 $profile = $notice->getProfile();
418 // We trim() to avoid extraneous whitespace in the output
420 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
421 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
422 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
423 $entry['published'] = common_date_iso8601($notice->created);
425 $taguribase = TagURI::base();
426 $entry['id'] = "tag:$taguribase:$entry[link]";
428 $entry['updated'] = $entry['published'];
429 $entry['author'] = $profile->getBestName();
432 $attachments = $notice->attachments();
433 $enclosures = array();
435 foreach ($attachments as $attachment) {
436 $enclosure_o=$attachment->getEnclosure();
438 $enclosure = array();
439 $enclosure['url'] = $enclosure_o->url;
440 $enclosure['mimetype'] = $enclosure_o->mimetype;
441 $enclosure['size'] = $enclosure_o->size;
442 $enclosures[] = $enclosure;
446 if (!empty($enclosures)) {
447 $entry['enclosures'] = $enclosures;
451 $tag = new Notice_tag();
452 $tag->notice_id = $notice->id;
454 $entry['tags']=array();
455 while ($tag->fetch()) {
456 $entry['tags'][]=$tag->tag;
462 $entry['description'] = $entry['content'];
463 $entry['pubDate'] = common_date_rfc2822($notice->created);
464 $entry['guid'] = $entry['link'];
466 if (isset($notice->lat) && isset($notice->lon)) {
467 // This is the format that GeoJSON expects stuff to be in.
468 // showGeoRSS() below uses it for XML output, so we reuse it
469 $entry['geo'] = array('type' => 'Point',
470 'coordinates' => array((float) $notice->lat,
471 (float) $notice->lon));
473 $entry['geo'] = null;
479 function twitterRelationshipArray($source, $target)
481 $relationship = array();
483 $relationship['source'] =
484 $this->relationshipDetailsArray($source, $target);
485 $relationship['target'] =
486 $this->relationshipDetailsArray($target, $source);
488 return array('relationship' => $relationship);
491 function relationshipDetailsArray($source, $target)
495 $details['screen_name'] = $source->nickname;
496 $details['followed_by'] = $target->isSubscribed($source);
497 $details['following'] = $source->isSubscribed($target);
499 $notifications = false;
501 if ($source->isSubscribed($target)) {
503 $sub = Subscription::pkeyGet(array('subscriber' =>
504 $source->id, 'subscribed' => $target->id));
507 $notifications = ($sub->jabber || $sub->sms);
511 $details['notifications_enabled'] = $notifications;
512 $details['blocking'] = $source->hasBlocked($target);
513 $details['id'] = $source->id;
518 function showTwitterXmlRelationship($relationship)
520 $this->elementStart('relationship');
522 foreach($relationship as $element => $value) {
523 if ($element == 'source' || $element == 'target') {
524 $this->elementStart($element);
525 $this->showXmlRelationshipDetails($value);
526 $this->elementEnd($element);
530 $this->elementEnd('relationship');
533 function showXmlRelationshipDetails($details)
535 foreach($details as $element => $value) {
536 $this->element($element, null, $value);
540 function showTwitterXmlStatus($twitter_status, $tag='status')
542 $this->elementStart($tag);
543 foreach($twitter_status as $element => $value) {
546 $this->showTwitterXmlUser($twitter_status['user']);
549 $this->element($element, null, common_xml_safe_str($value));
552 $this->showXmlAttachments($twitter_status['attachments']);
555 $this->showGeoXML($value);
557 case 'retweeted_status':
558 $this->showTwitterXmlStatus($value, 'retweeted_status');
561 $this->element($element, null, $value);
564 $this->elementEnd($tag);
567 function showTwitterXmlGroup($twitter_group)
569 $this->elementStart('group');
570 foreach($twitter_group as $element => $value) {
571 $this->element($element, null, $value);
573 $this->elementEnd('group');
576 function showTwitterXmlUser($twitter_user, $role='user')
578 $this->elementStart($role);
579 foreach($twitter_user as $element => $value) {
580 if ($element == 'status') {
581 $this->showTwitterXmlStatus($twitter_user['status']);
583 $this->element($element, null, $value);
586 $this->elementEnd($role);
589 function showXmlAttachments($attachments) {
590 if (!empty($attachments)) {
591 $this->elementStart('attachments', array('type' => 'array'));
592 foreach ($attachments as $attachment) {
594 $attrs['url'] = $attachment['url'];
595 $attrs['mimetype'] = $attachment['mimetype'];
596 $attrs['size'] = $attachment['size'];
597 $this->element('enclosure', $attrs, '');
599 $this->elementEnd('attachments');
603 function showGeoXML($geo)
607 $this->element('geo');
609 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
610 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
611 $this->elementEnd('geo');
615 function showGeoRSS($geo)
621 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
626 function showTwitterRssItem($entry)
628 $this->elementStart('item');
629 $this->element('title', null, $entry['title']);
630 $this->element('description', null, $entry['description']);
631 $this->element('pubDate', null, $entry['pubDate']);
632 $this->element('guid', null, $entry['guid']);
633 $this->element('link', null, $entry['link']);
635 # RSS only supports 1 enclosure per item
636 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
637 $enclosure = $entry['enclosures'][0];
638 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
641 if(array_key_exists('tags', $entry)){
642 foreach($entry['tags'] as $tag){
643 $this->element('category', null,$tag);
647 $this->showGeoRSS($entry['geo']);
648 $this->elementEnd('item');
651 function showJsonObjects($objects)
653 print(json_encode($objects));
656 function showSingleXmlStatus($notice)
658 $this->initDocument('xml');
659 $twitter_status = $this->twitterStatusArray($notice);
660 $this->showTwitterXmlStatus($twitter_status);
661 $this->endDocument('xml');
664 function show_single_json_status($notice)
666 $this->initDocument('json');
667 $status = $this->twitterStatusArray($notice);
668 $this->showJsonObjects($status);
669 $this->endDocument('json');
672 function showXmlTimeline($notice)
675 $this->initDocument('xml');
676 $this->elementStart('statuses', array('type' => 'array'));
678 if (is_array($notice)) {
679 foreach ($notice as $n) {
680 $twitter_status = $this->twitterStatusArray($n);
681 $this->showTwitterXmlStatus($twitter_status);
684 while ($notice->fetch()) {
685 $twitter_status = $this->twitterStatusArray($notice);
686 $this->showTwitterXmlStatus($twitter_status);
690 $this->elementEnd('statuses');
691 $this->endDocument('xml');
694 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
697 $this->initDocument('rss');
699 $this->element('title', null, $title);
700 $this->element('link', null, $link);
702 if (!is_null($self)) {
706 'type' => 'application/rss+xml',
713 if (!is_null($suplink)) {
714 // For FriendFeed's SUP protocol
715 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
716 'rel' => 'http://api.friendfeed.com/2008/03#sup',
718 'type' => 'application/json'));
721 if (!is_null($logo)) {
722 $this->elementStart('image');
723 $this->element('link', null, $link);
724 $this->element('title', null, $title);
725 $this->element('url', null, $logo);
726 $this->elementEnd('image');
729 $this->element('description', null, $subtitle);
730 $this->element('language', null, 'en-us');
731 $this->element('ttl', null, '40');
733 if (is_array($notice)) {
734 foreach ($notice as $n) {
735 $entry = $this->twitterRssEntryArray($n);
736 $this->showTwitterRssItem($entry);
739 while ($notice->fetch()) {
740 $entry = $this->twitterRssEntryArray($notice);
741 $this->showTwitterRssItem($entry);
745 $this->endTwitterRss();
748 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
751 $this->initDocument('atom');
753 $this->element('title', null, $title);
754 $this->element('id', null, $id);
755 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
757 if (!is_null($logo)) {
758 $this->element('logo',null,$logo);
761 if (!is_null($suplink)) {
762 # For FriendFeed's SUP protocol
763 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
765 'type' => 'application/json'));
768 if (!is_null($selfuri)) {
769 $this->element('link', array('href' => $selfuri,
770 'rel' => 'self', 'type' => 'application/atom+xml'), null);
773 $this->element('updated', null, common_date_iso8601('now'));
774 $this->element('subtitle', null, $subtitle);
776 if (is_array($notice)) {
777 foreach ($notice as $n) {
778 $this->raw($n->asAtomEntry());
781 while ($notice->fetch()) {
782 $this->raw($notice->asAtomEntry());
786 $this->endDocument('atom');
790 function showRssGroups($group, $title, $link, $subtitle)
793 $this->initDocument('rss');
795 $this->element('title', null, $title);
796 $this->element('link', null, $link);
797 $this->element('description', null, $subtitle);
798 $this->element('language', null, 'en-us');
799 $this->element('ttl', null, '40');
801 if (is_array($group)) {
802 foreach ($group as $g) {
803 $twitter_group = $this->twitterRssGroupArray($g);
804 $this->showTwitterRssItem($twitter_group);
807 while ($group->fetch()) {
808 $twitter_group = $this->twitterRssGroupArray($group);
809 $this->showTwitterRssItem($twitter_group);
813 $this->endTwitterRss();
816 function showTwitterAtomEntry($entry)
818 $this->elementStart('entry');
819 $this->element('title', null, common_xml_safe_str($entry['title']));
822 array('type' => 'html'),
823 common_xml_safe_str($entry['content'])
825 $this->element('id', null, $entry['id']);
826 $this->element('published', null, $entry['published']);
827 $this->element('updated', null, $entry['updated']);
828 $this->element('link', array('type' => 'text/html',
829 'href' => $entry['link'],
830 'rel' => 'alternate'));
831 $this->element('link', array('type' => $entry['avatar-type'],
832 'href' => $entry['avatar'],
834 $this->elementStart('author');
836 $this->element('name', null, $entry['author-name']);
837 $this->element('uri', null, $entry['author-uri']);
839 $this->elementEnd('author');
840 $this->elementEnd('entry');
843 function showXmlDirectMessage($dm)
845 $this->elementStart('direct_message');
846 foreach($dm as $element => $value) {
850 $this->showTwitterXmlUser($value, $element);
853 $this->element($element, null, common_xml_safe_str($value));
856 $this->element($element, null, $value);
860 $this->elementEnd('direct_message');
863 function directMessageArray($message)
867 $from_profile = $message->getFrom();
868 $to_profile = $message->getTo();
870 $dmsg['id'] = $message->id;
871 $dmsg['sender_id'] = $message->from_profile;
872 $dmsg['text'] = trim($message->content);
873 $dmsg['recipient_id'] = $message->to_profile;
874 $dmsg['created_at'] = $this->dateTwitter($message->created);
875 $dmsg['sender_screen_name'] = $from_profile->nickname;
876 $dmsg['recipient_screen_name'] = $to_profile->nickname;
877 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
878 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
883 function rssDirectMessageArray($message)
887 $from = $message->getFrom();
889 $entry['title'] = sprintf('Message from %1$s to %2$s',
890 $from->nickname, $message->getTo()->nickname);
892 $entry['content'] = common_xml_safe_str($message->rendered);
893 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
894 $entry['published'] = common_date_iso8601($message->created);
896 $taguribase = TagURI::base();
898 $entry['id'] = "tag:$taguribase:$entry[link]";
899 $entry['updated'] = $entry['published'];
901 $entry['author-name'] = $from->getBestName();
902 $entry['author-uri'] = $from->homepage;
904 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
906 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
907 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
911 $entry['description'] = $entry['content'];
912 $entry['pubDate'] = common_date_rfc2822($message->created);
913 $entry['guid'] = $entry['link'];
918 function showSingleXmlDirectMessage($message)
920 $this->initDocument('xml');
921 $dmsg = $this->directMessageArray($message);
922 $this->showXmlDirectMessage($dmsg);
923 $this->endDocument('xml');
926 function showSingleJsonDirectMessage($message)
928 $this->initDocument('json');
929 $dmsg = $this->directMessageArray($message);
930 $this->showJsonObjects($dmsg);
931 $this->endDocument('json');
934 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
937 $this->initDocument('atom');
939 $this->element('title', null, common_xml_safe_str($title));
940 $this->element('id', null, $id);
941 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
943 if (!is_null($selfuri)) {
944 $this->element('link', array('href' => $selfuri,
945 'rel' => 'self', 'type' => 'application/atom+xml'), null);
948 $this->element('updated', null, common_date_iso8601('now'));
949 $this->element('subtitle', null, common_xml_safe_str($subtitle));
951 if (is_array($group)) {
952 foreach ($group as $g) {
953 $this->raw($g->asAtomEntry());
956 while ($group->fetch()) {
957 $this->raw($group->asAtomEntry());
961 $this->endDocument('atom');
965 function showJsonTimeline($notice)
968 $this->initDocument('json');
972 if (is_array($notice)) {
973 foreach ($notice as $n) {
974 $twitter_status = $this->twitterStatusArray($n);
975 array_push($statuses, $twitter_status);
978 while ($notice->fetch()) {
979 $twitter_status = $this->twitterStatusArray($notice);
980 array_push($statuses, $twitter_status);
984 $this->showJsonObjects($statuses);
986 $this->endDocument('json');
989 function showJsonGroups($group)
992 $this->initDocument('json');
996 if (is_array($group)) {
997 foreach ($group as $g) {
998 $twitter_group = $this->twitterGroupArray($g);
999 array_push($groups, $twitter_group);
1002 while ($group->fetch()) {
1003 $twitter_group = $this->twitterGroupArray($group);
1004 array_push($groups, $twitter_group);
1008 $this->showJsonObjects($groups);
1010 $this->endDocument('json');
1013 function showXmlGroups($group)
1016 $this->initDocument('xml');
1017 $this->elementStart('groups', array('type' => 'array'));
1019 if (is_array($group)) {
1020 foreach ($group as $g) {
1021 $twitter_group = $this->twitterGroupArray($g);
1022 $this->showTwitterXmlGroup($twitter_group);
1025 while ($group->fetch()) {
1026 $twitter_group = $this->twitterGroupArray($group);
1027 $this->showTwitterXmlGroup($twitter_group);
1031 $this->elementEnd('groups');
1032 $this->endDocument('xml');
1035 function showTwitterXmlUsers($user)
1038 $this->initDocument('xml');
1039 $this->elementStart('users', array('type' => 'array'));
1041 if (is_array($user)) {
1042 foreach ($user as $u) {
1043 $twitter_user = $this->twitterUserArray($u);
1044 $this->showTwitterXmlUser($twitter_user);
1047 while ($user->fetch()) {
1048 $twitter_user = $this->twitterUserArray($user);
1049 $this->showTwitterXmlUser($twitter_user);
1053 $this->elementEnd('users');
1054 $this->endDocument('xml');
1057 function showJsonUsers($user)
1060 $this->initDocument('json');
1064 if (is_array($user)) {
1065 foreach ($user as $u) {
1066 $twitter_user = $this->twitterUserArray($u);
1067 array_push($users, $twitter_user);
1070 while ($user->fetch()) {
1071 $twitter_user = $this->twitterUserArray($user);
1072 array_push($users, $twitter_user);
1076 $this->showJsonObjects($users);
1078 $this->endDocument('json');
1081 function showSingleJsonGroup($group)
1083 $this->initDocument('json');
1084 $twitter_group = $this->twitterGroupArray($group);
1085 $this->showJsonObjects($twitter_group);
1086 $this->endDocument('json');
1089 function showSingleXmlGroup($group)
1091 $this->initDocument('xml');
1092 $twitter_group = $this->twitterGroupArray($group);
1093 $this->showTwitterXmlGroup($twitter_group);
1094 $this->endDocument('xml');
1097 function dateTwitter($dt)
1099 $dateStr = date('d F Y H:i:s', strtotime($dt));
1100 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1101 $d->setTimezone(new DateTimeZone(common_timezone()));
1102 return $d->format('D M d H:i:s O Y');
1105 function initDocument($type='xml')
1109 header('Content-Type: application/xml; charset=utf-8');
1113 header('Content-Type: application/json; charset=utf-8');
1115 // Check for JSONP callback
1116 $callback = $this->arg('callback');
1118 print $callback . '(';
1122 header("Content-Type: application/rss+xml; charset=utf-8");
1123 $this->initTwitterRss();
1126 header('Content-Type: application/atom+xml; charset=utf-8');
1127 $this->initTwitterAtom();
1130 // TRANS: Client error on an API request with an unsupported data format.
1131 $this->clientError(_('Not a supported data format.'));
1138 function endDocument($type='xml')
1146 // Check for JSONP callback
1147 $callback = $this->arg('callback');
1153 $this->endTwitterRss();
1156 $this->endTwitterRss();
1159 // TRANS: Client error on an API request with an unsupported data format.
1160 $this->clientError(_('Not a supported data format.'));
1166 function clientError($msg, $code = 400, $format = 'xml')
1168 $action = $this->trimmed('action');
1170 common_debug("User error '$code' on '$action': $msg", __FILE__);
1172 if (!array_key_exists($code, ClientErrorAction::$status)) {
1176 $status_string = ClientErrorAction::$status[$code];
1178 header('HTTP/1.1 '.$code.' '.$status_string);
1180 if ($format == 'xml') {
1181 $this->initDocument('xml');
1182 $this->elementStart('hash');
1183 $this->element('error', null, $msg);
1184 $this->element('request', null, $_SERVER['REQUEST_URI']);
1185 $this->elementEnd('hash');
1186 $this->endDocument('xml');
1187 } elseif ($format == 'json'){
1188 $this->initDocument('json');
1189 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1190 print(json_encode($error_array));
1191 $this->endDocument('json');
1194 // If user didn't request a useful format, throw a regular client error
1195 throw new ClientException($msg, $code);
1199 function serverError($msg, $code = 500, $content_type = 'xml')
1201 $action = $this->trimmed('action');
1203 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1205 if (!array_key_exists($code, ServerErrorAction::$status)) {
1209 $status_string = ServerErrorAction::$status[$code];
1211 header('HTTP/1.1 '.$code.' '.$status_string);
1213 if ($content_type == 'xml') {
1214 $this->initDocument('xml');
1215 $this->elementStart('hash');
1216 $this->element('error', null, $msg);
1217 $this->element('request', null, $_SERVER['REQUEST_URI']);
1218 $this->elementEnd('hash');
1219 $this->endDocument('xml');
1221 $this->initDocument('json');
1222 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1223 print(json_encode($error_array));
1224 $this->endDocument('json');
1228 function initTwitterRss()
1231 $this->elementStart(
1235 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1236 'xmlns:georss' => 'http://www.georss.org/georss'
1239 $this->elementStart('channel');
1240 Event::handle('StartApiRss', array($this));
1243 function endTwitterRss()
1245 $this->elementEnd('channel');
1246 $this->elementEnd('rss');
1250 function initTwitterAtom()
1253 // FIXME: don't hardcode the language here!
1254 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1255 'xml:lang' => 'en-US',
1256 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1259 function endTwitterAtom()
1261 $this->elementEnd('feed');
1265 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1267 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1268 switch ($content_type) {
1270 $this->showTwitterXmlUser($profile_array);
1273 $this->showJsonObjects($profile_array);
1276 // TRANS: Client error on an API request with an unsupported data format.
1277 $this->clientError(_('Not a supported data format.'));
1283 function getTargetUser($id)
1287 // Twitter supports these other ways of passing the user ID
1288 if (is_numeric($this->arg('id'))) {
1289 return User::staticGet($this->arg('id'));
1290 } else if ($this->arg('id')) {
1291 $nickname = common_canonical_nickname($this->arg('id'));
1292 return User::staticGet('nickname', $nickname);
1293 } else if ($this->arg('user_id')) {
1294 // This is to ensure that a non-numeric user_id still
1295 // overrides screen_name even if it doesn't get used
1296 if (is_numeric($this->arg('user_id'))) {
1297 return User::staticGet('id', $this->arg('user_id'));
1299 } else if ($this->arg('screen_name')) {
1300 $nickname = common_canonical_nickname($this->arg('screen_name'));
1301 return User::staticGet('nickname', $nickname);
1303 // Fall back to trying the currently authenticated user
1304 return $this->auth_user;
1307 } else if (is_numeric($id)) {
1308 return User::staticGet($id);
1310 $nickname = common_canonical_nickname($id);
1311 return User::staticGet('nickname', $nickname);
1315 function getTargetGroup($id)
1318 if (is_numeric($this->arg('id'))) {
1319 return User_group::staticGet($this->arg('id'));
1320 } else if ($this->arg('id')) {
1321 $nickname = common_canonical_nickname($this->arg('id'));
1322 $local = Local_group::staticGet('nickname', $nickname);
1323 if (empty($local)) {
1326 return User_group::staticGet('id', $local->id);
1328 } else if ($this->arg('group_id')) {
1329 // This is to ensure that a non-numeric user_id still
1330 // overrides screen_name even if it doesn't get used
1331 if (is_numeric($this->arg('group_id'))) {
1332 return User_group::staticGet('id', $this->arg('group_id'));
1334 } else if ($this->arg('group_name')) {
1335 $nickname = common_canonical_nickname($this->arg('group_name'));
1336 $local = Local_group::staticGet('nickname', $nickname);
1337 if (empty($local)) {
1340 return User_group::staticGet('id', $local->group_id);
1344 } else if (is_numeric($id)) {
1345 return User_group::staticGet($id);
1347 $nickname = common_canonical_nickname($id);
1348 $local = Local_group::staticGet('nickname', $nickname);
1349 if (empty($local)) {
1352 return User_group::staticGet('id', $local->group_id);
1357 function sourceLink($source)
1359 $source_name = _($source);
1372 $ns = Notice_source::staticGet($source);
1378 $app = Oauth_application::staticGet('name', $source);
1381 $url = $app->source_url;
1385 if (!empty($name) && !empty($url)) {
1386 $source_name = '<a href="' . $url . '">' . $name . '</a>';
1391 return $source_name;
1395 * Returns query argument or default value if not found. Certain
1396 * parameters used throughout the API are lightly scrubbed and
1397 * bounds checked. This overrides Action::arg().
1399 * @param string $key requested argument
1400 * @param string $def default value to return if $key is not provided
1404 function arg($key, $def=null)
1407 // XXX: Do even more input validation/scrubbing?
1409 if (array_key_exists($key, $this->args)) {
1412 $page = (int)$this->args['page'];
1413 return ($page < 1) ? 1 : $page;
1415 $count = (int)$this->args['count'];
1418 } elseif ($count > 200) {
1424 $since_id = (int)$this->args['since_id'];
1425 return ($since_id < 1) ? 0 : $since_id;
1427 $max_id = (int)$this->args['max_id'];
1428 return ($max_id < 1) ? 0 : $max_id;
1430 return parent::arg($key, $def);
1438 * Calculate the complete URI that called up this action. Used for
1439 * Atom rel="self" links. Warning: this is funky.
1441 * @return string URL a URL suitable for rel="self" Atom links
1443 function getSelfUri()
1445 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1447 $id = $this->arg('id');
1448 $aargs = array('format' => $this->format);
1453 $tag = $this->arg('tag');
1455 $aargs['tag'] = $tag;
1458 parse_str($_SERVER['QUERY_STRING'], $params);
1460 if (!empty($params)) {
1461 unset($params['p']);
1462 $pstring = http_build_query($params);
1465 $uri = common_local_url($action, $aargs);
1467 if (!empty($pstring)) {
1468 $uri .= '?' . $pstring;