3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009 StatusNet, Inc.
31 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33 * @link http://status.net/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
77 The URLs for methods referred to in this API documentation are
78 relative to the StatusNet API root. The API root is determined by the
79 site's @b server and @b path variables, which are generally specified
80 in config.php. For example:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
108 * @author Craig Andrews <candrews@integralblue.com>
109 * @author Dan Moore <dan@moore.cx>
110 * @author Evan Prodromou <evan@status.net>
111 * @author Jeffery To <jeffery.to@gmail.com>
112 * @author Toby Inkster <mail@tobyinkster.co.uk>
113 * @author Zach Copley <zach@status.net>
114 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115 * @link http://status.net/
118 class ApiAction extends Action
121 const READ_WRITE = 2;
125 var $auth_user = null;
129 var $since_id = null;
132 var $access = self::READ_ONLY; // read (default) or read-write
134 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
139 * @param array $args Web and URL arguments
141 * @return boolean false if user doesn't exist
144 function prepare($args)
146 StatusNet::setApi(true); // reduce exception reports to aid in debugging
147 parent::prepare($args);
149 $this->format = $this->arg('format');
150 $this->page = (int)$this->arg('page', 1);
151 $this->count = (int)$this->arg('count', 20);
152 $this->max_id = (int)$this->arg('max_id', 0);
153 $this->since_id = (int)$this->arg('since_id', 0);
155 if ($this->arg('since')) {
156 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
159 $this->source = $this->trimmed('source');
161 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
162 $this->source = 'api';
171 * @param array $args Arguments from $_REQUEST
176 function handle($args)
178 header('Access-Control-Allow-Origin: *');
179 parent::handle($args);
183 * Overrides XMLOutputter::element to write booleans as strings (true|false).
184 * See that method's documentation for more info.
186 * @param string $tag Element type or tagname
187 * @param array $attrs Array of element attributes, as
189 * @param string $content string content of the element
193 function element($tag, $attrs=null, $content=null)
195 if (is_bool($content)) {
196 $content = ($content ? 'true' : 'false');
199 return parent::element($tag, $attrs, $content);
202 function twitterUserArray($profile, $get_notice=false)
204 $twitter_user = array();
206 $twitter_user['id'] = intval($profile->id);
207 $twitter_user['name'] = $profile->getBestName();
208 $twitter_user['screen_name'] = $profile->nickname;
209 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
210 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
212 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
213 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
214 Avatar::defaultImage(AVATAR_STREAM_SIZE);
216 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
217 $twitter_user['protected'] = false; # not supported by StatusNet yet
218 $twitter_user['followers_count'] = $profile->subscriberCount();
221 $user = $profile->getUser();
223 // Note: some profiles don't have an associated user
225 $defaultDesign = Design::siteDesign();
228 $design = $user->getDesign();
231 if (empty($design)) {
232 $design = $defaultDesign;
235 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
236 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
238 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
240 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
242 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243 $twitter_user['profile_sidebar_border_color'] = '';
245 $twitter_user['friends_count'] = $profile->subscriptionCount();
247 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
249 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
253 if (!empty($user) && $user->timezone) {
254 $timezone = $user->timezone;
258 $t->setTimezone(new DateTimeZone($timezone));
260 $twitter_user['utc_offset'] = $t->format('Z');
261 $twitter_user['time_zone'] = $timezone;
263 $twitter_user['profile_background_image_url']
264 = empty($design->backgroundimage)
265 ? '' : ($design->disposition & BACKGROUND_ON)
266 ? Design::url($design->backgroundimage) : '';
268 $twitter_user['profile_background_tile']
269 = empty($design->disposition)
270 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
272 $twitter_user['statuses_count'] = $profile->noticeCount();
274 // Is the requesting user following this user?
275 $twitter_user['following'] = false;
276 $twitter_user['notifications'] = false;
278 if (isset($this->auth_user)) {
280 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
283 $sub = Subscription::pkeyGet(array('subscriber' =>
284 $this->auth_user->id,
285 'subscribed' => $profile->id));
288 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
293 $notice = $profile->getCurrentNotice();
296 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
300 return $twitter_user;
303 function twitterStatusArray($notice, $include_user=true)
305 $base = $this->twitterSimpleStatusArray($notice, $include_user);
307 if (!empty($notice->repeat_of)) {
308 $original = Notice::staticGet('id', $notice->repeat_of);
309 if (!empty($original)) {
310 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
311 $base['retweeted_status'] = $original_array;
318 function twitterSimpleStatusArray($notice, $include_user=true)
320 $profile = $notice->getProfile();
322 $twitter_status = array();
323 $twitter_status['text'] = $notice->content;
324 $twitter_status['truncated'] = false; # Not possible on StatusNet
325 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
326 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
327 intval($notice->reply_to) : null;
331 $ns = $notice->getSource();
333 if (!empty($ns->name) && !empty($ns->url)) {
334 $source = '<a href="'
335 . htmlspecialchars($ns->url)
336 . '" rel="nofollow">'
337 . htmlspecialchars($ns->name)
344 $twitter_status['source'] = $source;
345 $twitter_status['id'] = intval($notice->id);
347 $replier_profile = null;
349 if ($notice->reply_to) {
350 $reply = Notice::staticGet(intval($notice->reply_to));
352 $replier_profile = $reply->getProfile();
356 $twitter_status['in_reply_to_user_id'] =
357 ($replier_profile) ? intval($replier_profile->id) : null;
358 $twitter_status['in_reply_to_screen_name'] =
359 ($replier_profile) ? $replier_profile->nickname : null;
361 if (isset($notice->lat) && isset($notice->lon)) {
362 // This is the format that GeoJSON expects stuff to be in
363 $twitter_status['geo'] = array('type' => 'Point',
364 'coordinates' => array((float) $notice->lat,
365 (float) $notice->lon));
367 $twitter_status['geo'] = null;
370 if (isset($this->auth_user)) {
371 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
373 $twitter_status['favorited'] = false;
377 $attachments = $notice->attachments();
379 if (!empty($attachments)) {
381 $twitter_status['attachments'] = array();
383 foreach ($attachments as $attachment) {
384 $enclosure_o=$attachment->getEnclosure();
386 $enclosure = array();
387 $enclosure['url'] = $enclosure_o->url;
388 $enclosure['mimetype'] = $enclosure_o->mimetype;
389 $enclosure['size'] = $enclosure_o->size;
390 $twitter_status['attachments'][] = $enclosure;
395 if ($include_user && $profile) {
396 # Don't get notice (recursive!)
397 $twitter_user = $this->twitterUserArray($profile, false);
398 $twitter_status['user'] = $twitter_user;
401 return $twitter_status;
404 function twitterGroupArray($group)
406 $twitter_group=array();
407 $twitter_group['id']=$group->id;
408 $twitter_group['url']=$group->permalink();
409 $twitter_group['nickname']=$group->nickname;
410 $twitter_group['fullname']=$group->fullname;
411 $twitter_group['original_logo']=$group->original_logo;
412 $twitter_group['homepage_logo']=$group->homepage_logo;
413 $twitter_group['stream_logo']=$group->stream_logo;
414 $twitter_group['mini_logo']=$group->mini_logo;
415 $twitter_group['homepage']=$group->homepage;
416 $twitter_group['description']=$group->description;
417 $twitter_group['location']=$group->location;
418 $twitter_group['created']=$this->dateTwitter($group->created);
419 $twitter_group['modified']=$this->dateTwitter($group->modified);
420 return $twitter_group;
423 function twitterRssGroupArray($group)
426 $entry['content']=$group->description;
427 $entry['title']=$group->nickname;
428 $entry['link']=$group->permalink();
429 $entry['published']=common_date_iso8601($group->created);
430 $entry['updated']==common_date_iso8601($group->modified);
431 $taguribase = common_config('integration', 'groupuri');
432 $entry['id'] = "group:$groupuribase:$entry[link]";
434 $entry['description'] = $entry['content'];
435 $entry['pubDate'] = common_date_rfc2822($group->created);
436 $entry['guid'] = $entry['link'];
441 function twitterRssEntryArray($notice)
443 $profile = $notice->getProfile();
446 // We trim() to avoid extraneous whitespace in the output
448 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
449 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
450 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
451 $entry['published'] = common_date_iso8601($notice->created);
453 $taguribase = TagURI::base();
454 $entry['id'] = "tag:$taguribase:$entry[link]";
456 $entry['updated'] = $entry['published'];
457 $entry['author'] = $profile->getBestName();
460 $attachments = $notice->attachments();
461 $enclosures = array();
463 foreach ($attachments as $attachment) {
464 $enclosure_o=$attachment->getEnclosure();
466 $enclosure = array();
467 $enclosure['url'] = $enclosure_o->url;
468 $enclosure['mimetype'] = $enclosure_o->mimetype;
469 $enclosure['size'] = $enclosure_o->size;
470 $enclosures[] = $enclosure;
474 if (!empty($enclosures)) {
475 $entry['enclosures'] = $enclosures;
479 $tag = new Notice_tag();
480 $tag->notice_id = $notice->id;
482 $entry['tags']=array();
483 while ($tag->fetch()) {
484 $entry['tags'][]=$tag->tag;
490 $entry['description'] = $entry['content'];
491 $entry['pubDate'] = common_date_rfc2822($notice->created);
492 $entry['guid'] = $entry['link'];
494 if (isset($notice->lat) && isset($notice->lon)) {
495 // This is the format that GeoJSON expects stuff to be in.
496 // showGeoRSS() below uses it for XML output, so we reuse it
497 $entry['geo'] = array('type' => 'Point',
498 'coordinates' => array((float) $notice->lat,
499 (float) $notice->lon));
501 $entry['geo'] = null;
507 function twitterRelationshipArray($source, $target)
509 $relationship = array();
511 $relationship['source'] =
512 $this->relationshipDetailsArray($source, $target);
513 $relationship['target'] =
514 $this->relationshipDetailsArray($target, $source);
516 return array('relationship' => $relationship);
519 function relationshipDetailsArray($source, $target)
523 $details['screen_name'] = $source->nickname;
524 $details['followed_by'] = $target->isSubscribed($source);
525 $details['following'] = $source->isSubscribed($target);
527 $notifications = false;
529 if ($source->isSubscribed($target)) {
531 $sub = Subscription::pkeyGet(array('subscriber' =>
532 $source->id, 'subscribed' => $target->id));
535 $notifications = ($sub->jabber || $sub->sms);
539 $details['notifications_enabled'] = $notifications;
540 $details['blocking'] = $source->hasBlocked($target);
541 $details['id'] = $source->id;
546 function showTwitterXmlRelationship($relationship)
548 $this->elementStart('relationship');
550 foreach($relationship as $element => $value) {
551 if ($element == 'source' || $element == 'target') {
552 $this->elementStart($element);
553 $this->showXmlRelationshipDetails($value);
554 $this->elementEnd($element);
558 $this->elementEnd('relationship');
561 function showXmlRelationshipDetails($details)
563 foreach($details as $element => $value) {
564 $this->element($element, null, $value);
568 function showTwitterXmlStatus($twitter_status, $tag='status')
570 $this->elementStart($tag);
571 foreach($twitter_status as $element => $value) {
574 $this->showTwitterXmlUser($twitter_status['user']);
577 $this->element($element, null, common_xml_safe_str($value));
580 $this->showXmlAttachments($twitter_status['attachments']);
583 $this->showGeoXML($value);
585 case 'retweeted_status':
586 $this->showTwitterXmlStatus($value, 'retweeted_status');
589 $this->element($element, null, $value);
592 $this->elementEnd($tag);
595 function showTwitterXmlGroup($twitter_group)
597 $this->elementStart('group');
598 foreach($twitter_group as $element => $value) {
599 $this->element($element, null, $value);
601 $this->elementEnd('group');
604 function showTwitterXmlUser($twitter_user, $role='user')
606 $this->elementStart($role);
607 foreach($twitter_user as $element => $value) {
608 if ($element == 'status') {
609 $this->showTwitterXmlStatus($twitter_user['status']);
611 $this->element($element, null, $value);
614 $this->elementEnd($role);
617 function showXmlAttachments($attachments) {
618 if (!empty($attachments)) {
619 $this->elementStart('attachments', array('type' => 'array'));
620 foreach ($attachments as $attachment) {
622 $attrs['url'] = $attachment['url'];
623 $attrs['mimetype'] = $attachment['mimetype'];
624 $attrs['size'] = $attachment['size'];
625 $this->element('enclosure', $attrs, '');
627 $this->elementEnd('attachments');
631 function showGeoXML($geo)
635 $this->element('geo');
637 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
638 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
639 $this->elementEnd('geo');
643 function showGeoRSS($geo)
649 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
654 function showTwitterRssItem($entry)
656 $this->elementStart('item');
657 $this->element('title', null, $entry['title']);
658 $this->element('description', null, $entry['description']);
659 $this->element('pubDate', null, $entry['pubDate']);
660 $this->element('guid', null, $entry['guid']);
661 $this->element('link', null, $entry['link']);
663 # RSS only supports 1 enclosure per item
664 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
665 $enclosure = $entry['enclosures'][0];
666 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
669 if(array_key_exists('tags', $entry)){
670 foreach($entry['tags'] as $tag){
671 $this->element('category', null,$tag);
675 $this->showGeoRSS($entry['geo']);
676 $this->elementEnd('item');
679 function showJsonObjects($objects)
681 print(json_encode($objects));
684 function showSingleXmlStatus($notice)
686 $this->initDocument('xml');
687 $twitter_status = $this->twitterStatusArray($notice);
688 $this->showTwitterXmlStatus($twitter_status);
689 $this->endDocument('xml');
692 function show_single_json_status($notice)
694 $this->initDocument('json');
695 $status = $this->twitterStatusArray($notice);
696 $this->showJsonObjects($status);
697 $this->endDocument('json');
700 function showXmlTimeline($notice)
703 $this->initDocument('xml');
704 $this->elementStart('statuses', array('type' => 'array'));
706 if (is_array($notice)) {
707 foreach ($notice as $n) {
708 $twitter_status = $this->twitterStatusArray($n);
709 $this->showTwitterXmlStatus($twitter_status);
712 while ($notice->fetch()) {
713 $twitter_status = $this->twitterStatusArray($notice);
714 $this->showTwitterXmlStatus($twitter_status);
718 $this->elementEnd('statuses');
719 $this->endDocument('xml');
722 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
725 $this->initDocument('rss');
727 $this->element('title', null, $title);
728 $this->element('link', null, $link);
730 if (!is_null($self)) {
734 'type' => 'application/rss+xml',
741 if (!is_null($suplink)) {
742 // For FriendFeed's SUP protocol
743 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
744 'rel' => 'http://api.friendfeed.com/2008/03#sup',
746 'type' => 'application/json'));
749 if (!is_null($logo)) {
750 $this->elementStart('image');
751 $this->element('link', null, $link);
752 $this->element('title', null, $title);
753 $this->element('url', null, $logo);
754 $this->elementEnd('image');
757 $this->element('description', null, $subtitle);
758 $this->element('language', null, 'en-us');
759 $this->element('ttl', null, '40');
761 if (is_array($notice)) {
762 foreach ($notice as $n) {
763 $entry = $this->twitterRssEntryArray($n);
764 $this->showTwitterRssItem($entry);
767 while ($notice->fetch()) {
768 $entry = $this->twitterRssEntryArray($notice);
769 $this->showTwitterRssItem($entry);
773 $this->endTwitterRss();
776 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
779 $this->initDocument('atom');
781 $this->element('title', null, $title);
782 $this->element('id', null, $id);
783 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
785 if (!is_null($logo)) {
786 $this->element('logo',null,$logo);
789 if (!is_null($suplink)) {
790 # For FriendFeed's SUP protocol
791 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
793 'type' => 'application/json'));
796 if (!is_null($selfuri)) {
797 $this->element('link', array('href' => $selfuri,
798 'rel' => 'self', 'type' => 'application/atom+xml'), null);
801 $this->element('updated', null, common_date_iso8601('now'));
802 $this->element('subtitle', null, $subtitle);
804 if (is_array($notice)) {
805 foreach ($notice as $n) {
806 $this->raw($n->asAtomEntry());
809 while ($notice->fetch()) {
810 $this->raw($notice->asAtomEntry());
814 $this->endDocument('atom');
818 function showRssGroups($group, $title, $link, $subtitle)
821 $this->initDocument('rss');
823 $this->element('title', null, $title);
824 $this->element('link', null, $link);
825 $this->element('description', null, $subtitle);
826 $this->element('language', null, 'en-us');
827 $this->element('ttl', null, '40');
829 if (is_array($group)) {
830 foreach ($group as $g) {
831 $twitter_group = $this->twitterRssGroupArray($g);
832 $this->showTwitterRssItem($twitter_group);
835 while ($group->fetch()) {
836 $twitter_group = $this->twitterRssGroupArray($group);
837 $this->showTwitterRssItem($twitter_group);
841 $this->endTwitterRss();
844 function showTwitterAtomEntry($entry)
846 $this->elementStart('entry');
847 $this->element('title', null, common_xml_safe_str($entry['title']));
850 array('type' => 'html'),
851 common_xml_safe_str($entry['content'])
853 $this->element('id', null, $entry['id']);
854 $this->element('published', null, $entry['published']);
855 $this->element('updated', null, $entry['updated']);
856 $this->element('link', array('type' => 'text/html',
857 'href' => $entry['link'],
858 'rel' => 'alternate'));
859 $this->element('link', array('type' => $entry['avatar-type'],
860 'href' => $entry['avatar'],
862 $this->elementStart('author');
864 $this->element('name', null, $entry['author-name']);
865 $this->element('uri', null, $entry['author-uri']);
867 $this->elementEnd('author');
868 $this->elementEnd('entry');
871 function showXmlDirectMessage($dm)
873 $this->elementStart('direct_message');
874 foreach($dm as $element => $value) {
878 $this->showTwitterXmlUser($value, $element);
881 $this->element($element, null, common_xml_safe_str($value));
884 $this->element($element, null, $value);
888 $this->elementEnd('direct_message');
891 function directMessageArray($message)
895 $from_profile = $message->getFrom();
896 $to_profile = $message->getTo();
898 $dmsg['id'] = $message->id;
899 $dmsg['sender_id'] = $message->from_profile;
900 $dmsg['text'] = trim($message->content);
901 $dmsg['recipient_id'] = $message->to_profile;
902 $dmsg['created_at'] = $this->dateTwitter($message->created);
903 $dmsg['sender_screen_name'] = $from_profile->nickname;
904 $dmsg['recipient_screen_name'] = $to_profile->nickname;
905 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
906 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
911 function rssDirectMessageArray($message)
915 $from = $message->getFrom();
917 $entry['title'] = sprintf('Message from %1$s to %2$s',
918 $from->nickname, $message->getTo()->nickname);
920 $entry['content'] = common_xml_safe_str($message->rendered);
921 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
922 $entry['published'] = common_date_iso8601($message->created);
924 $taguribase = TagURI::base();
926 $entry['id'] = "tag:$taguribase:$entry[link]";
927 $entry['updated'] = $entry['published'];
929 $entry['author-name'] = $from->getBestName();
930 $entry['author-uri'] = $from->homepage;
932 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
934 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
935 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
939 $entry['description'] = $entry['content'];
940 $entry['pubDate'] = common_date_rfc2822($message->created);
941 $entry['guid'] = $entry['link'];
946 function showSingleXmlDirectMessage($message)
948 $this->initDocument('xml');
949 $dmsg = $this->directMessageArray($message);
950 $this->showXmlDirectMessage($dmsg);
951 $this->endDocument('xml');
954 function showSingleJsonDirectMessage($message)
956 $this->initDocument('json');
957 $dmsg = $this->directMessageArray($message);
958 $this->showJsonObjects($dmsg);
959 $this->endDocument('json');
962 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
965 $this->initDocument('atom');
967 $this->element('title', null, common_xml_safe_str($title));
968 $this->element('id', null, $id);
969 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
971 if (!is_null($selfuri)) {
972 $this->element('link', array('href' => $selfuri,
973 'rel' => 'self', 'type' => 'application/atom+xml'), null);
976 $this->element('updated', null, common_date_iso8601('now'));
977 $this->element('subtitle', null, common_xml_safe_str($subtitle));
979 if (is_array($group)) {
980 foreach ($group as $g) {
981 $this->raw($g->asAtomEntry());
984 while ($group->fetch()) {
985 $this->raw($group->asAtomEntry());
989 $this->endDocument('atom');
993 function showJsonTimeline($notice)
996 $this->initDocument('json');
1000 if (is_array($notice)) {
1001 foreach ($notice as $n) {
1002 $twitter_status = $this->twitterStatusArray($n);
1003 array_push($statuses, $twitter_status);
1006 while ($notice->fetch()) {
1007 $twitter_status = $this->twitterStatusArray($notice);
1008 array_push($statuses, $twitter_status);
1012 $this->showJsonObjects($statuses);
1014 $this->endDocument('json');
1017 function showJsonGroups($group)
1020 $this->initDocument('json');
1024 if (is_array($group)) {
1025 foreach ($group as $g) {
1026 $twitter_group = $this->twitterGroupArray($g);
1027 array_push($groups, $twitter_group);
1030 while ($group->fetch()) {
1031 $twitter_group = $this->twitterGroupArray($group);
1032 array_push($groups, $twitter_group);
1036 $this->showJsonObjects($groups);
1038 $this->endDocument('json');
1041 function showXmlGroups($group)
1044 $this->initDocument('xml');
1045 $this->elementStart('groups', array('type' => 'array'));
1047 if (is_array($group)) {
1048 foreach ($group as $g) {
1049 $twitter_group = $this->twitterGroupArray($g);
1050 $this->showTwitterXmlGroup($twitter_group);
1053 while ($group->fetch()) {
1054 $twitter_group = $this->twitterGroupArray($group);
1055 $this->showTwitterXmlGroup($twitter_group);
1059 $this->elementEnd('groups');
1060 $this->endDocument('xml');
1063 function showTwitterXmlUsers($user)
1066 $this->initDocument('xml');
1067 $this->elementStart('users', array('type' => 'array'));
1069 if (is_array($user)) {
1070 foreach ($user as $u) {
1071 $twitter_user = $this->twitterUserArray($u);
1072 $this->showTwitterXmlUser($twitter_user);
1075 while ($user->fetch()) {
1076 $twitter_user = $this->twitterUserArray($user);
1077 $this->showTwitterXmlUser($twitter_user);
1081 $this->elementEnd('users');
1082 $this->endDocument('xml');
1085 function showJsonUsers($user)
1088 $this->initDocument('json');
1092 if (is_array($user)) {
1093 foreach ($user as $u) {
1094 $twitter_user = $this->twitterUserArray($u);
1095 array_push($users, $twitter_user);
1098 while ($user->fetch()) {
1099 $twitter_user = $this->twitterUserArray($user);
1100 array_push($users, $twitter_user);
1104 $this->showJsonObjects($users);
1106 $this->endDocument('json');
1109 function showSingleJsonGroup($group)
1111 $this->initDocument('json');
1112 $twitter_group = $this->twitterGroupArray($group);
1113 $this->showJsonObjects($twitter_group);
1114 $this->endDocument('json');
1117 function showSingleXmlGroup($group)
1119 $this->initDocument('xml');
1120 $twitter_group = $this->twitterGroupArray($group);
1121 $this->showTwitterXmlGroup($twitter_group);
1122 $this->endDocument('xml');
1125 function dateTwitter($dt)
1127 $dateStr = date('d F Y H:i:s', strtotime($dt));
1128 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1129 $d->setTimezone(new DateTimeZone(common_timezone()));
1130 return $d->format('D M d H:i:s O Y');
1133 function initDocument($type='xml')
1137 header('Content-Type: application/xml; charset=utf-8');
1141 header('Content-Type: application/json; charset=utf-8');
1143 // Check for JSONP callback
1144 $callback = $this->arg('callback');
1146 print $callback . '(';
1150 header("Content-Type: application/rss+xml; charset=utf-8");
1151 $this->initTwitterRss();
1154 header('Content-Type: application/atom+xml; charset=utf-8');
1155 $this->initTwitterAtom();
1158 // TRANS: Client error on an API request with an unsupported data format.
1159 $this->clientError(_('Not a supported data format.'));
1166 function endDocument($type='xml')
1174 // Check for JSONP callback
1175 $callback = $this->arg('callback');
1181 $this->endTwitterRss();
1184 $this->endTwitterRss();
1187 // TRANS: Client error on an API request with an unsupported data format.
1188 $this->clientError(_('Not a supported data format.'));
1194 function clientError($msg, $code = 400, $format = 'xml')
1196 $action = $this->trimmed('action');
1198 common_debug("User error '$code' on '$action': $msg", __FILE__);
1200 if (!array_key_exists($code, ClientErrorAction::$status)) {
1204 $status_string = ClientErrorAction::$status[$code];
1206 header('HTTP/1.1 '.$code.' '.$status_string);
1208 if ($format == 'xml') {
1209 $this->initDocument('xml');
1210 $this->elementStart('hash');
1211 $this->element('error', null, $msg);
1212 $this->element('request', null, $_SERVER['REQUEST_URI']);
1213 $this->elementEnd('hash');
1214 $this->endDocument('xml');
1215 } elseif ($format == 'json'){
1216 $this->initDocument('json');
1217 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1218 print(json_encode($error_array));
1219 $this->endDocument('json');
1222 // If user didn't request a useful format, throw a regular client error
1223 throw new ClientException($msg, $code);
1227 function serverError($msg, $code = 500, $content_type = 'xml')
1229 $action = $this->trimmed('action');
1231 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1233 if (!array_key_exists($code, ServerErrorAction::$status)) {
1237 $status_string = ServerErrorAction::$status[$code];
1239 header('HTTP/1.1 '.$code.' '.$status_string);
1241 if ($content_type == 'xml') {
1242 $this->initDocument('xml');
1243 $this->elementStart('hash');
1244 $this->element('error', null, $msg);
1245 $this->element('request', null, $_SERVER['REQUEST_URI']);
1246 $this->elementEnd('hash');
1247 $this->endDocument('xml');
1249 $this->initDocument('json');
1250 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1251 print(json_encode($error_array));
1252 $this->endDocument('json');
1256 function initTwitterRss()
1259 $this->elementStart(
1263 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1264 'xmlns:georss' => 'http://www.georss.org/georss'
1267 $this->elementStart('channel');
1268 Event::handle('StartApiRss', array($this));
1271 function endTwitterRss()
1273 $this->elementEnd('channel');
1274 $this->elementEnd('rss');
1278 function initTwitterAtom()
1281 // FIXME: don't hardcode the language here!
1282 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1283 'xml:lang' => 'en-US',
1284 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1287 function endTwitterAtom()
1289 $this->elementEnd('feed');
1293 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1295 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1296 switch ($content_type) {
1298 $this->showTwitterXmlUser($profile_array);
1301 $this->showJsonObjects($profile_array);
1304 // TRANS: Client error on an API request with an unsupported data format.
1305 $this->clientError(_('Not a supported data format.'));
1311 function getTargetUser($id)
1315 // Twitter supports these other ways of passing the user ID
1316 if (is_numeric($this->arg('id'))) {
1317 return User::staticGet($this->arg('id'));
1318 } else if ($this->arg('id')) {
1319 $nickname = common_canonical_nickname($this->arg('id'));
1320 return User::staticGet('nickname', $nickname);
1321 } else if ($this->arg('user_id')) {
1322 // This is to ensure that a non-numeric user_id still
1323 // overrides screen_name even if it doesn't get used
1324 if (is_numeric($this->arg('user_id'))) {
1325 return User::staticGet('id', $this->arg('user_id'));
1327 } else if ($this->arg('screen_name')) {
1328 $nickname = common_canonical_nickname($this->arg('screen_name'));
1329 return User::staticGet('nickname', $nickname);
1331 // Fall back to trying the currently authenticated user
1332 return $this->auth_user;
1335 } else if (is_numeric($id)) {
1336 return User::staticGet($id);
1338 $nickname = common_canonical_nickname($id);
1339 return User::staticGet('nickname', $nickname);
1343 function getTargetGroup($id)
1346 if (is_numeric($this->arg('id'))) {
1347 return User_group::staticGet($this->arg('id'));
1348 } else if ($this->arg('id')) {
1349 $nickname = common_canonical_nickname($this->arg('id'));
1350 $local = Local_group::staticGet('nickname', $nickname);
1351 if (empty($local)) {
1354 return User_group::staticGet('id', $local->id);
1356 } else if ($this->arg('group_id')) {
1357 // This is to ensure that a non-numeric user_id still
1358 // overrides screen_name even if it doesn't get used
1359 if (is_numeric($this->arg('group_id'))) {
1360 return User_group::staticGet('id', $this->arg('group_id'));
1362 } else if ($this->arg('group_name')) {
1363 $nickname = common_canonical_nickname($this->arg('group_name'));
1364 $local = Local_group::staticGet('nickname', $nickname);
1365 if (empty($local)) {
1368 return User_group::staticGet('id', $local->group_id);
1372 } else if (is_numeric($id)) {
1373 return User_group::staticGet($id);
1375 $nickname = common_canonical_nickname($id);
1376 $local = Local_group::staticGet('nickname', $nickname);
1377 if (empty($local)) {
1380 return User_group::staticGet('id', $local->group_id);
1386 * Returns query argument or default value if not found. Certain
1387 * parameters used throughout the API are lightly scrubbed and
1388 * bounds checked. This overrides Action::arg().
1390 * @param string $key requested argument
1391 * @param string $def default value to return if $key is not provided
1395 function arg($key, $def=null)
1398 // XXX: Do even more input validation/scrubbing?
1400 if (array_key_exists($key, $this->args)) {
1403 $page = (int)$this->args['page'];
1404 return ($page < 1) ? 1 : $page;
1406 $count = (int)$this->args['count'];
1409 } elseif ($count > 200) {
1415 $since_id = (int)$this->args['since_id'];
1416 return ($since_id < 1) ? 0 : $since_id;
1418 $max_id = (int)$this->args['max_id'];
1419 return ($max_id < 1) ? 0 : $max_id;
1421 return parent::arg($key, $def);
1429 * Calculate the complete URI that called up this action. Used for
1430 * Atom rel="self" links. Warning: this is funky.
1432 * @return string URL a URL suitable for rel="self" Atom links
1434 function getSelfUri()
1436 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1438 $id = $this->arg('id');
1439 $aargs = array('format' => $this->format);
1444 $tag = $this->arg('tag');
1446 $aargs['tag'] = $tag;
1449 parse_str($_SERVER['QUERY_STRING'], $params);
1451 if (!empty($params)) {
1452 unset($params['p']);
1453 $pstring = http_build_query($params);
1456 $uri = common_local_url($action, $aargs);
1458 if (!empty($pstring)) {
1459 $uri .= '?' . $pstring;