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="'
323 . htmlspecialchars($ns->url)
324 . '" rel="nofollow">'
325 . htmlspecialchars($ns->name)
332 $twitter_status['source'] = $source;
333 $twitter_status['id'] = intval($notice->id);
335 $replier_profile = null;
337 if ($notice->reply_to) {
338 $reply = Notice::staticGet(intval($notice->reply_to));
340 $replier_profile = $reply->getProfile();
344 $twitter_status['in_reply_to_user_id'] =
345 ($replier_profile) ? intval($replier_profile->id) : null;
346 $twitter_status['in_reply_to_screen_name'] =
347 ($replier_profile) ? $replier_profile->nickname : null;
349 if (isset($notice->lat) && isset($notice->lon)) {
350 // This is the format that GeoJSON expects stuff to be in
351 $twitter_status['geo'] = array('type' => 'Point',
352 'coordinates' => array((float) $notice->lat,
353 (float) $notice->lon));
355 $twitter_status['geo'] = null;
358 if (isset($this->auth_user)) {
359 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
361 $twitter_status['favorited'] = false;
365 $attachments = $notice->attachments();
367 if (!empty($attachments)) {
369 $twitter_status['attachments'] = array();
371 foreach ($attachments as $attachment) {
372 $enclosure_o=$attachment->getEnclosure();
374 $enclosure = array();
375 $enclosure['url'] = $enclosure_o->url;
376 $enclosure['mimetype'] = $enclosure_o->mimetype;
377 $enclosure['size'] = $enclosure_o->size;
378 $twitter_status['attachments'][] = $enclosure;
383 if ($include_user && $profile) {
384 # Don't get notice (recursive!)
385 $twitter_user = $this->twitterUserArray($profile, false);
386 $twitter_status['user'] = $twitter_user;
389 return $twitter_status;
392 function twitterGroupArray($group)
394 $twitter_group=array();
395 $twitter_group['id']=$group->id;
396 $twitter_group['url']=$group->permalink();
397 $twitter_group['nickname']=$group->nickname;
398 $twitter_group['fullname']=$group->fullname;
399 $twitter_group['original_logo']=$group->original_logo;
400 $twitter_group['homepage_logo']=$group->homepage_logo;
401 $twitter_group['stream_logo']=$group->stream_logo;
402 $twitter_group['mini_logo']=$group->mini_logo;
403 $twitter_group['homepage']=$group->homepage;
404 $twitter_group['description']=$group->description;
405 $twitter_group['location']=$group->location;
406 $twitter_group['created']=$this->dateTwitter($group->created);
407 $twitter_group['modified']=$this->dateTwitter($group->modified);
408 return $twitter_group;
411 function twitterRssGroupArray($group)
414 $entry['content']=$group->description;
415 $entry['title']=$group->nickname;
416 $entry['link']=$group->permalink();
417 $entry['published']=common_date_iso8601($group->created);
418 $entry['updated']==common_date_iso8601($group->modified);
419 $taguribase = common_config('integration', 'groupuri');
420 $entry['id'] = "group:$groupuribase:$entry[link]";
422 $entry['description'] = $entry['content'];
423 $entry['pubDate'] = common_date_rfc2822($group->created);
424 $entry['guid'] = $entry['link'];
429 function twitterRssEntryArray($notice)
431 $profile = $notice->getProfile();
434 // We trim() to avoid extraneous whitespace in the output
436 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
437 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
438 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
439 $entry['published'] = common_date_iso8601($notice->created);
441 $taguribase = TagURI::base();
442 $entry['id'] = "tag:$taguribase:$entry[link]";
444 $entry['updated'] = $entry['published'];
445 $entry['author'] = $profile->getBestName();
448 $attachments = $notice->attachments();
449 $enclosures = array();
451 foreach ($attachments as $attachment) {
452 $enclosure_o=$attachment->getEnclosure();
454 $enclosure = array();
455 $enclosure['url'] = $enclosure_o->url;
456 $enclosure['mimetype'] = $enclosure_o->mimetype;
457 $enclosure['size'] = $enclosure_o->size;
458 $enclosures[] = $enclosure;
462 if (!empty($enclosures)) {
463 $entry['enclosures'] = $enclosures;
467 $tag = new Notice_tag();
468 $tag->notice_id = $notice->id;
470 $entry['tags']=array();
471 while ($tag->fetch()) {
472 $entry['tags'][]=$tag->tag;
478 $entry['description'] = $entry['content'];
479 $entry['pubDate'] = common_date_rfc2822($notice->created);
480 $entry['guid'] = $entry['link'];
482 if (isset($notice->lat) && isset($notice->lon)) {
483 // This is the format that GeoJSON expects stuff to be in.
484 // showGeoRSS() below uses it for XML output, so we reuse it
485 $entry['geo'] = array('type' => 'Point',
486 'coordinates' => array((float) $notice->lat,
487 (float) $notice->lon));
489 $entry['geo'] = null;
495 function twitterRelationshipArray($source, $target)
497 $relationship = array();
499 $relationship['source'] =
500 $this->relationshipDetailsArray($source, $target);
501 $relationship['target'] =
502 $this->relationshipDetailsArray($target, $source);
504 return array('relationship' => $relationship);
507 function relationshipDetailsArray($source, $target)
511 $details['screen_name'] = $source->nickname;
512 $details['followed_by'] = $target->isSubscribed($source);
513 $details['following'] = $source->isSubscribed($target);
515 $notifications = false;
517 if ($source->isSubscribed($target)) {
519 $sub = Subscription::pkeyGet(array('subscriber' =>
520 $source->id, 'subscribed' => $target->id));
523 $notifications = ($sub->jabber || $sub->sms);
527 $details['notifications_enabled'] = $notifications;
528 $details['blocking'] = $source->hasBlocked($target);
529 $details['id'] = $source->id;
534 function showTwitterXmlRelationship($relationship)
536 $this->elementStart('relationship');
538 foreach($relationship as $element => $value) {
539 if ($element == 'source' || $element == 'target') {
540 $this->elementStart($element);
541 $this->showXmlRelationshipDetails($value);
542 $this->elementEnd($element);
546 $this->elementEnd('relationship');
549 function showXmlRelationshipDetails($details)
551 foreach($details as $element => $value) {
552 $this->element($element, null, $value);
556 function showTwitterXmlStatus($twitter_status, $tag='status')
558 $this->elementStart($tag);
559 foreach($twitter_status as $element => $value) {
562 $this->showTwitterXmlUser($twitter_status['user']);
565 $this->element($element, null, common_xml_safe_str($value));
568 $this->showXmlAttachments($twitter_status['attachments']);
571 $this->showGeoXML($value);
573 case 'retweeted_status':
574 $this->showTwitterXmlStatus($value, 'retweeted_status');
577 $this->element($element, null, $value);
580 $this->elementEnd($tag);
583 function showTwitterXmlGroup($twitter_group)
585 $this->elementStart('group');
586 foreach($twitter_group as $element => $value) {
587 $this->element($element, null, $value);
589 $this->elementEnd('group');
592 function showTwitterXmlUser($twitter_user, $role='user')
594 $this->elementStart($role);
595 foreach($twitter_user as $element => $value) {
596 if ($element == 'status') {
597 $this->showTwitterXmlStatus($twitter_user['status']);
599 $this->element($element, null, $value);
602 $this->elementEnd($role);
605 function showXmlAttachments($attachments) {
606 if (!empty($attachments)) {
607 $this->elementStart('attachments', array('type' => 'array'));
608 foreach ($attachments as $attachment) {
610 $attrs['url'] = $attachment['url'];
611 $attrs['mimetype'] = $attachment['mimetype'];
612 $attrs['size'] = $attachment['size'];
613 $this->element('enclosure', $attrs, '');
615 $this->elementEnd('attachments');
619 function showGeoXML($geo)
623 $this->element('geo');
625 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
626 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
627 $this->elementEnd('geo');
631 function showGeoRSS($geo)
637 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
642 function showTwitterRssItem($entry)
644 $this->elementStart('item');
645 $this->element('title', null, $entry['title']);
646 $this->element('description', null, $entry['description']);
647 $this->element('pubDate', null, $entry['pubDate']);
648 $this->element('guid', null, $entry['guid']);
649 $this->element('link', null, $entry['link']);
651 # RSS only supports 1 enclosure per item
652 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
653 $enclosure = $entry['enclosures'][0];
654 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
657 if(array_key_exists('tags', $entry)){
658 foreach($entry['tags'] as $tag){
659 $this->element('category', null,$tag);
663 $this->showGeoRSS($entry['geo']);
664 $this->elementEnd('item');
667 function showJsonObjects($objects)
669 print(json_encode($objects));
672 function showSingleXmlStatus($notice)
674 $this->initDocument('xml');
675 $twitter_status = $this->twitterStatusArray($notice);
676 $this->showTwitterXmlStatus($twitter_status);
677 $this->endDocument('xml');
680 function show_single_json_status($notice)
682 $this->initDocument('json');
683 $status = $this->twitterStatusArray($notice);
684 $this->showJsonObjects($status);
685 $this->endDocument('json');
688 function showXmlTimeline($notice)
691 $this->initDocument('xml');
692 $this->elementStart('statuses', array('type' => 'array'));
694 if (is_array($notice)) {
695 foreach ($notice as $n) {
696 $twitter_status = $this->twitterStatusArray($n);
697 $this->showTwitterXmlStatus($twitter_status);
700 while ($notice->fetch()) {
701 $twitter_status = $this->twitterStatusArray($notice);
702 $this->showTwitterXmlStatus($twitter_status);
706 $this->elementEnd('statuses');
707 $this->endDocument('xml');
710 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
713 $this->initDocument('rss');
715 $this->element('title', null, $title);
716 $this->element('link', null, $link);
718 if (!is_null($self)) {
722 'type' => 'application/rss+xml',
729 if (!is_null($suplink)) {
730 // For FriendFeed's SUP protocol
731 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
732 'rel' => 'http://api.friendfeed.com/2008/03#sup',
734 'type' => 'application/json'));
737 if (!is_null($logo)) {
738 $this->elementStart('image');
739 $this->element('link', null, $link);
740 $this->element('title', null, $title);
741 $this->element('url', null, $logo);
742 $this->elementEnd('image');
745 $this->element('description', null, $subtitle);
746 $this->element('language', null, 'en-us');
747 $this->element('ttl', null, '40');
749 if (is_array($notice)) {
750 foreach ($notice as $n) {
751 $entry = $this->twitterRssEntryArray($n);
752 $this->showTwitterRssItem($entry);
755 while ($notice->fetch()) {
756 $entry = $this->twitterRssEntryArray($notice);
757 $this->showTwitterRssItem($entry);
761 $this->endTwitterRss();
764 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
767 $this->initDocument('atom');
769 $this->element('title', null, $title);
770 $this->element('id', null, $id);
771 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
773 if (!is_null($logo)) {
774 $this->element('logo',null,$logo);
777 if (!is_null($suplink)) {
778 # For FriendFeed's SUP protocol
779 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
781 'type' => 'application/json'));
784 if (!is_null($selfuri)) {
785 $this->element('link', array('href' => $selfuri,
786 'rel' => 'self', 'type' => 'application/atom+xml'), null);
789 $this->element('updated', null, common_date_iso8601('now'));
790 $this->element('subtitle', null, $subtitle);
792 if (is_array($notice)) {
793 foreach ($notice as $n) {
794 $this->raw($n->asAtomEntry());
797 while ($notice->fetch()) {
798 $this->raw($notice->asAtomEntry());
802 $this->endDocument('atom');
806 function showRssGroups($group, $title, $link, $subtitle)
809 $this->initDocument('rss');
811 $this->element('title', null, $title);
812 $this->element('link', null, $link);
813 $this->element('description', null, $subtitle);
814 $this->element('language', null, 'en-us');
815 $this->element('ttl', null, '40');
817 if (is_array($group)) {
818 foreach ($group as $g) {
819 $twitter_group = $this->twitterRssGroupArray($g);
820 $this->showTwitterRssItem($twitter_group);
823 while ($group->fetch()) {
824 $twitter_group = $this->twitterRssGroupArray($group);
825 $this->showTwitterRssItem($twitter_group);
829 $this->endTwitterRss();
832 function showTwitterAtomEntry($entry)
834 $this->elementStart('entry');
835 $this->element('title', null, common_xml_safe_str($entry['title']));
838 array('type' => 'html'),
839 common_xml_safe_str($entry['content'])
841 $this->element('id', null, $entry['id']);
842 $this->element('published', null, $entry['published']);
843 $this->element('updated', null, $entry['updated']);
844 $this->element('link', array('type' => 'text/html',
845 'href' => $entry['link'],
846 'rel' => 'alternate'));
847 $this->element('link', array('type' => $entry['avatar-type'],
848 'href' => $entry['avatar'],
850 $this->elementStart('author');
852 $this->element('name', null, $entry['author-name']);
853 $this->element('uri', null, $entry['author-uri']);
855 $this->elementEnd('author');
856 $this->elementEnd('entry');
859 function showXmlDirectMessage($dm)
861 $this->elementStart('direct_message');
862 foreach($dm as $element => $value) {
866 $this->showTwitterXmlUser($value, $element);
869 $this->element($element, null, common_xml_safe_str($value));
872 $this->element($element, null, $value);
876 $this->elementEnd('direct_message');
879 function directMessageArray($message)
883 $from_profile = $message->getFrom();
884 $to_profile = $message->getTo();
886 $dmsg['id'] = $message->id;
887 $dmsg['sender_id'] = $message->from_profile;
888 $dmsg['text'] = trim($message->content);
889 $dmsg['recipient_id'] = $message->to_profile;
890 $dmsg['created_at'] = $this->dateTwitter($message->created);
891 $dmsg['sender_screen_name'] = $from_profile->nickname;
892 $dmsg['recipient_screen_name'] = $to_profile->nickname;
893 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
894 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
899 function rssDirectMessageArray($message)
903 $from = $message->getFrom();
905 $entry['title'] = sprintf('Message from %1$s to %2$s',
906 $from->nickname, $message->getTo()->nickname);
908 $entry['content'] = common_xml_safe_str($message->rendered);
909 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
910 $entry['published'] = common_date_iso8601($message->created);
912 $taguribase = TagURI::base();
914 $entry['id'] = "tag:$taguribase:$entry[link]";
915 $entry['updated'] = $entry['published'];
917 $entry['author-name'] = $from->getBestName();
918 $entry['author-uri'] = $from->homepage;
920 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
922 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
923 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
927 $entry['description'] = $entry['content'];
928 $entry['pubDate'] = common_date_rfc2822($message->created);
929 $entry['guid'] = $entry['link'];
934 function showSingleXmlDirectMessage($message)
936 $this->initDocument('xml');
937 $dmsg = $this->directMessageArray($message);
938 $this->showXmlDirectMessage($dmsg);
939 $this->endDocument('xml');
942 function showSingleJsonDirectMessage($message)
944 $this->initDocument('json');
945 $dmsg = $this->directMessageArray($message);
946 $this->showJsonObjects($dmsg);
947 $this->endDocument('json');
950 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
953 $this->initDocument('atom');
955 $this->element('title', null, common_xml_safe_str($title));
956 $this->element('id', null, $id);
957 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
959 if (!is_null($selfuri)) {
960 $this->element('link', array('href' => $selfuri,
961 'rel' => 'self', 'type' => 'application/atom+xml'), null);
964 $this->element('updated', null, common_date_iso8601('now'));
965 $this->element('subtitle', null, common_xml_safe_str($subtitle));
967 if (is_array($group)) {
968 foreach ($group as $g) {
969 $this->raw($g->asAtomEntry());
972 while ($group->fetch()) {
973 $this->raw($group->asAtomEntry());
977 $this->endDocument('atom');
981 function showJsonTimeline($notice)
984 $this->initDocument('json');
988 if (is_array($notice)) {
989 foreach ($notice as $n) {
990 $twitter_status = $this->twitterStatusArray($n);
991 array_push($statuses, $twitter_status);
994 while ($notice->fetch()) {
995 $twitter_status = $this->twitterStatusArray($notice);
996 array_push($statuses, $twitter_status);
1000 $this->showJsonObjects($statuses);
1002 $this->endDocument('json');
1005 function showJsonGroups($group)
1008 $this->initDocument('json');
1012 if (is_array($group)) {
1013 foreach ($group as $g) {
1014 $twitter_group = $this->twitterGroupArray($g);
1015 array_push($groups, $twitter_group);
1018 while ($group->fetch()) {
1019 $twitter_group = $this->twitterGroupArray($group);
1020 array_push($groups, $twitter_group);
1024 $this->showJsonObjects($groups);
1026 $this->endDocument('json');
1029 function showXmlGroups($group)
1032 $this->initDocument('xml');
1033 $this->elementStart('groups', array('type' => 'array'));
1035 if (is_array($group)) {
1036 foreach ($group as $g) {
1037 $twitter_group = $this->twitterGroupArray($g);
1038 $this->showTwitterXmlGroup($twitter_group);
1041 while ($group->fetch()) {
1042 $twitter_group = $this->twitterGroupArray($group);
1043 $this->showTwitterXmlGroup($twitter_group);
1047 $this->elementEnd('groups');
1048 $this->endDocument('xml');
1051 function showTwitterXmlUsers($user)
1054 $this->initDocument('xml');
1055 $this->elementStart('users', array('type' => 'array'));
1057 if (is_array($user)) {
1058 foreach ($user as $u) {
1059 $twitter_user = $this->twitterUserArray($u);
1060 $this->showTwitterXmlUser($twitter_user);
1063 while ($user->fetch()) {
1064 $twitter_user = $this->twitterUserArray($user);
1065 $this->showTwitterXmlUser($twitter_user);
1069 $this->elementEnd('users');
1070 $this->endDocument('xml');
1073 function showJsonUsers($user)
1076 $this->initDocument('json');
1080 if (is_array($user)) {
1081 foreach ($user as $u) {
1082 $twitter_user = $this->twitterUserArray($u);
1083 array_push($users, $twitter_user);
1086 while ($user->fetch()) {
1087 $twitter_user = $this->twitterUserArray($user);
1088 array_push($users, $twitter_user);
1092 $this->showJsonObjects($users);
1094 $this->endDocument('json');
1097 function showSingleJsonGroup($group)
1099 $this->initDocument('json');
1100 $twitter_group = $this->twitterGroupArray($group);
1101 $this->showJsonObjects($twitter_group);
1102 $this->endDocument('json');
1105 function showSingleXmlGroup($group)
1107 $this->initDocument('xml');
1108 $twitter_group = $this->twitterGroupArray($group);
1109 $this->showTwitterXmlGroup($twitter_group);
1110 $this->endDocument('xml');
1113 function dateTwitter($dt)
1115 $dateStr = date('d F Y H:i:s', strtotime($dt));
1116 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1117 $d->setTimezone(new DateTimeZone(common_timezone()));
1118 return $d->format('D M d H:i:s O Y');
1121 function initDocument($type='xml')
1125 header('Content-Type: application/xml; charset=utf-8');
1129 header('Content-Type: application/json; charset=utf-8');
1131 // Check for JSONP callback
1132 $callback = $this->arg('callback');
1134 print $callback . '(';
1138 header("Content-Type: application/rss+xml; charset=utf-8");
1139 $this->initTwitterRss();
1142 header('Content-Type: application/atom+xml; charset=utf-8');
1143 $this->initTwitterAtom();
1146 // TRANS: Client error on an API request with an unsupported data format.
1147 $this->clientError(_('Not a supported data format.'));
1154 function endDocument($type='xml')
1162 // Check for JSONP callback
1163 $callback = $this->arg('callback');
1169 $this->endTwitterRss();
1172 $this->endTwitterRss();
1175 // TRANS: Client error on an API request with an unsupported data format.
1176 $this->clientError(_('Not a supported data format.'));
1182 function clientError($msg, $code = 400, $format = 'xml')
1184 $action = $this->trimmed('action');
1186 common_debug("User error '$code' on '$action': $msg", __FILE__);
1188 if (!array_key_exists($code, ClientErrorAction::$status)) {
1192 $status_string = ClientErrorAction::$status[$code];
1194 header('HTTP/1.1 '.$code.' '.$status_string);
1196 if ($format == 'xml') {
1197 $this->initDocument('xml');
1198 $this->elementStart('hash');
1199 $this->element('error', null, $msg);
1200 $this->element('request', null, $_SERVER['REQUEST_URI']);
1201 $this->elementEnd('hash');
1202 $this->endDocument('xml');
1203 } elseif ($format == 'json'){
1204 $this->initDocument('json');
1205 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1206 print(json_encode($error_array));
1207 $this->endDocument('json');
1210 // If user didn't request a useful format, throw a regular client error
1211 throw new ClientException($msg, $code);
1215 function serverError($msg, $code = 500, $content_type = 'xml')
1217 $action = $this->trimmed('action');
1219 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1221 if (!array_key_exists($code, ServerErrorAction::$status)) {
1225 $status_string = ServerErrorAction::$status[$code];
1227 header('HTTP/1.1 '.$code.' '.$status_string);
1229 if ($content_type == 'xml') {
1230 $this->initDocument('xml');
1231 $this->elementStart('hash');
1232 $this->element('error', null, $msg);
1233 $this->element('request', null, $_SERVER['REQUEST_URI']);
1234 $this->elementEnd('hash');
1235 $this->endDocument('xml');
1237 $this->initDocument('json');
1238 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1239 print(json_encode($error_array));
1240 $this->endDocument('json');
1244 function initTwitterRss()
1247 $this->elementStart(
1251 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1252 'xmlns:georss' => 'http://www.georss.org/georss'
1255 $this->elementStart('channel');
1256 Event::handle('StartApiRss', array($this));
1259 function endTwitterRss()
1261 $this->elementEnd('channel');
1262 $this->elementEnd('rss');
1266 function initTwitterAtom()
1269 // FIXME: don't hardcode the language here!
1270 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1271 'xml:lang' => 'en-US',
1272 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1275 function endTwitterAtom()
1277 $this->elementEnd('feed');
1281 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1283 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1284 switch ($content_type) {
1286 $this->showTwitterXmlUser($profile_array);
1289 $this->showJsonObjects($profile_array);
1292 // TRANS: Client error on an API request with an unsupported data format.
1293 $this->clientError(_('Not a supported data format.'));
1299 function getTargetUser($id)
1303 // Twitter supports these other ways of passing the user ID
1304 if (is_numeric($this->arg('id'))) {
1305 return User::staticGet($this->arg('id'));
1306 } else if ($this->arg('id')) {
1307 $nickname = common_canonical_nickname($this->arg('id'));
1308 return User::staticGet('nickname', $nickname);
1309 } else if ($this->arg('user_id')) {
1310 // This is to ensure that a non-numeric user_id still
1311 // overrides screen_name even if it doesn't get used
1312 if (is_numeric($this->arg('user_id'))) {
1313 return User::staticGet('id', $this->arg('user_id'));
1315 } else if ($this->arg('screen_name')) {
1316 $nickname = common_canonical_nickname($this->arg('screen_name'));
1317 return User::staticGet('nickname', $nickname);
1319 // Fall back to trying the currently authenticated user
1320 return $this->auth_user;
1323 } else if (is_numeric($id)) {
1324 return User::staticGet($id);
1326 $nickname = common_canonical_nickname($id);
1327 return User::staticGet('nickname', $nickname);
1331 function getTargetGroup($id)
1334 if (is_numeric($this->arg('id'))) {
1335 return User_group::staticGet($this->arg('id'));
1336 } else if ($this->arg('id')) {
1337 $nickname = common_canonical_nickname($this->arg('id'));
1338 $local = Local_group::staticGet('nickname', $nickname);
1339 if (empty($local)) {
1342 return User_group::staticGet('id', $local->id);
1344 } else if ($this->arg('group_id')) {
1345 // This is to ensure that a non-numeric user_id still
1346 // overrides screen_name even if it doesn't get used
1347 if (is_numeric($this->arg('group_id'))) {
1348 return User_group::staticGet('id', $this->arg('group_id'));
1350 } else if ($this->arg('group_name')) {
1351 $nickname = common_canonical_nickname($this->arg('group_name'));
1352 $local = Local_group::staticGet('nickname', $nickname);
1353 if (empty($local)) {
1356 return User_group::staticGet('id', $local->group_id);
1360 } else if (is_numeric($id)) {
1361 return User_group::staticGet($id);
1363 $nickname = common_canonical_nickname($id);
1364 $local = Local_group::staticGet('nickname', $nickname);
1365 if (empty($local)) {
1368 return User_group::staticGet('id', $local->group_id);
1374 * Returns query argument or default value if not found. Certain
1375 * parameters used throughout the API are lightly scrubbed and
1376 * bounds checked. This overrides Action::arg().
1378 * @param string $key requested argument
1379 * @param string $def default value to return if $key is not provided
1383 function arg($key, $def=null)
1386 // XXX: Do even more input validation/scrubbing?
1388 if (array_key_exists($key, $this->args)) {
1391 $page = (int)$this->args['page'];
1392 return ($page < 1) ? 1 : $page;
1394 $count = (int)$this->args['count'];
1397 } elseif ($count > 200) {
1403 $since_id = (int)$this->args['since_id'];
1404 return ($since_id < 1) ? 0 : $since_id;
1406 $max_id = (int)$this->args['max_id'];
1407 return ($max_id < 1) ? 0 : $max_id;
1409 return parent::arg($key, $def);
1417 * Calculate the complete URI that called up this action. Used for
1418 * Atom rel="self" links. Warning: this is funky.
1420 * @return string URL a URL suitable for rel="self" Atom links
1422 function getSelfUri()
1424 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1426 $id = $this->arg('id');
1427 $aargs = array('format' => $this->format);
1432 $tag = $this->arg('tag');
1434 $aargs['tag'] = $tag;
1437 parse_str($_SERVER['QUERY_STRING'], $params);
1439 if (!empty($params)) {
1440 unset($params['p']);
1441 $pstring = http_build_query($params);
1444 $uri = common_local_url($action, $aargs);
1446 if (!empty($pstring)) {
1447 $uri .= '?' . $pstring;