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')) {
102 * Contains most of the Twitter-compatible API output functions.
106 * @author Craig Andrews <candrews@integralblue.com>
107 * @author Dan Moore <dan@moore.cx>
108 * @author Evan Prodromou <evan@status.net>
109 * @author Jeffery To <jeffery.to@gmail.com>
110 * @author Toby Inkster <mail@tobyinkster.co.uk>
111 * @author Zach Copley <zach@status.net>
112 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
113 * @link http://status.net/
116 class ApiAction extends Action
119 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
130 var $access = self::READ_ONLY; // read (default) or read-write
132 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
137 * @param array $args Web and URL arguments
139 * @return boolean false if user doesn't exist
142 function prepare($args)
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
148 $this->page = (int)$this->arg('page', 1);
149 $this->count = (int)$this->arg('count', 20);
150 $this->max_id = (int)$this->arg('max_id', 0);
151 $this->since_id = (int)$this->arg('since_id', 0);
153 if ($this->arg('since')) {
154 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
157 $this->source = $this->trimmed('source');
159 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
160 $this->source = 'api';
169 * @param array $args Arguments from $_REQUEST
174 function handle($args)
176 header('Access-Control-Allow-Origin: *');
177 parent::handle($args);
181 * Overrides XMLOutputter::element to write booleans as strings (true|false).
182 * See that method's documentation for more info.
184 * @param string $tag Element type or tagname
185 * @param array $attrs Array of element attributes, as
187 * @param string $content string content of the element
191 function element($tag, $attrs=null, $content=null)
193 if (is_bool($content)) {
194 $content = ($content ? 'true' : 'false');
197 return parent::element($tag, $attrs, $content);
200 function twitterUserArray($profile, $get_notice=false)
202 $twitter_user = array();
204 $twitter_user['id'] = intval($profile->id);
205 $twitter_user['name'] = $profile->getBestName();
206 $twitter_user['screen_name'] = $profile->nickname;
207 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
208 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
210 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
211 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
212 Avatar::defaultImage(AVATAR_STREAM_SIZE);
214 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
215 $twitter_user['protected'] = false; # not supported by StatusNet yet
216 $twitter_user['followers_count'] = $profile->subscriberCount();
219 $user = $profile->getUser();
221 // Note: some profiles don't have an associated user
223 $defaultDesign = Design::siteDesign();
226 $design = $user->getDesign();
229 if (empty($design)) {
230 $design = $defaultDesign;
233 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
234 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
235 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
236 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
238 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
240 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $twitter_user['profile_sidebar_border_color'] = '';
243 $twitter_user['friends_count'] = $profile->subscriptionCount();
245 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
247 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
251 if (!empty($user) && $user->timezone) {
252 $timezone = $user->timezone;
256 $t->setTimezone(new DateTimeZone($timezone));
258 $twitter_user['utc_offset'] = $t->format('Z');
259 $twitter_user['time_zone'] = $timezone;
261 $twitter_user['profile_background_image_url']
262 = empty($design->backgroundimage)
263 ? '' : ($design->disposition & BACKGROUND_ON)
264 ? Design::url($design->backgroundimage) : '';
266 $twitter_user['profile_background_tile']
267 = empty($design->disposition)
268 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
270 $twitter_user['statuses_count'] = $profile->noticeCount();
272 // Is the requesting user following this user?
273 $twitter_user['following'] = false;
274 $twitter_user['notifications'] = false;
276 if (isset($this->auth_user)) {
278 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
281 $sub = Subscription::pkeyGet(array('subscriber' =>
282 $this->auth_user->id,
283 'subscribed' => $profile->id));
286 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
291 $notice = $profile->getCurrentNotice();
294 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
298 // StatusNet-specific
300 $twitter_user['statusnet:profile_url'] = $profile->profileurl;
302 return $twitter_user;
305 function twitterStatusArray($notice, $include_user=true)
307 $base = $this->twitterSimpleStatusArray($notice, $include_user);
309 if (!empty($notice->repeat_of)) {
310 $original = Notice::staticGet('id', $notice->repeat_of);
311 if (!empty($original)) {
312 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
313 $base['retweeted_status'] = $original_array;
320 function twitterSimpleStatusArray($notice, $include_user=true)
322 $profile = $notice->getProfile();
324 $twitter_status = array();
325 $twitter_status['text'] = $notice->content;
326 $twitter_status['truncated'] = false; # Not possible on StatusNet
327 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
328 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
329 intval($notice->reply_to) : null;
333 $ns = $notice->getSource();
335 if (!empty($ns->name) && !empty($ns->url)) {
336 $source = '<a href="'
337 . htmlspecialchars($ns->url)
338 . '" rel="nofollow">'
339 . htmlspecialchars($ns->name)
346 $twitter_status['source'] = $source;
347 $twitter_status['id'] = intval($notice->id);
349 $replier_profile = null;
351 if ($notice->reply_to) {
352 $reply = Notice::staticGet(intval($notice->reply_to));
354 $replier_profile = $reply->getProfile();
358 $twitter_status['in_reply_to_user_id'] =
359 ($replier_profile) ? intval($replier_profile->id) : null;
360 $twitter_status['in_reply_to_screen_name'] =
361 ($replier_profile) ? $replier_profile->nickname : null;
363 if (isset($notice->lat) && isset($notice->lon)) {
364 // This is the format that GeoJSON expects stuff to be in
365 $twitter_status['geo'] = array('type' => 'Point',
366 'coordinates' => array((float) $notice->lat,
367 (float) $notice->lon));
369 $twitter_status['geo'] = null;
372 if (isset($this->auth_user)) {
373 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
375 $twitter_status['favorited'] = false;
379 $attachments = $notice->attachments();
381 if (!empty($attachments)) {
383 $twitter_status['attachments'] = array();
385 foreach ($attachments as $attachment) {
386 $enclosure_o=$attachment->getEnclosure();
388 $enclosure = array();
389 $enclosure['url'] = $enclosure_o->url;
390 $enclosure['mimetype'] = $enclosure_o->mimetype;
391 $enclosure['size'] = $enclosure_o->size;
392 $twitter_status['attachments'][] = $enclosure;
397 if ($include_user && $profile) {
398 # Don't get notice (recursive!)
399 $twitter_user = $this->twitterUserArray($profile, false);
400 $twitter_status['user'] = $twitter_user;
403 // StatusNet-specific
405 $twitter_status['statusnet:html'] = $notice->rendered;
407 return $twitter_status;
410 function twitterGroupArray($group)
412 $twitter_group=array();
413 $twitter_group['id']=$group->id;
414 $twitter_group['url']=$group->permalink();
415 $twitter_group['nickname']=$group->nickname;
416 $twitter_group['fullname']=$group->fullname;
417 $twitter_group['original_logo']=$group->original_logo;
418 $twitter_group['homepage_logo']=$group->homepage_logo;
419 $twitter_group['stream_logo']=$group->stream_logo;
420 $twitter_group['mini_logo']=$group->mini_logo;
421 $twitter_group['homepage']=$group->homepage;
422 $twitter_group['description']=$group->description;
423 $twitter_group['location']=$group->location;
424 $twitter_group['created']=$this->dateTwitter($group->created);
425 $twitter_group['modified']=$this->dateTwitter($group->modified);
426 return $twitter_group;
429 function twitterRssGroupArray($group)
432 $entry['content']=$group->description;
433 $entry['title']=$group->nickname;
434 $entry['link']=$group->permalink();
435 $entry['published']=common_date_iso8601($group->created);
436 $entry['updated']==common_date_iso8601($group->modified);
437 $taguribase = common_config('integration', 'groupuri');
438 $entry['id'] = "group:$groupuribase:$entry[link]";
440 $entry['description'] = $entry['content'];
441 $entry['pubDate'] = common_date_rfc2822($group->created);
442 $entry['guid'] = $entry['link'];
447 function twitterRssEntryArray($notice)
449 $profile = $notice->getProfile();
452 // We trim() to avoid extraneous whitespace in the output
454 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
455 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
456 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
457 $entry['published'] = common_date_iso8601($notice->created);
459 $taguribase = TagURI::base();
460 $entry['id'] = "tag:$taguribase:$entry[link]";
462 $entry['updated'] = $entry['published'];
463 $entry['author'] = $profile->getBestName();
466 $attachments = $notice->attachments();
467 $enclosures = array();
469 foreach ($attachments as $attachment) {
470 $enclosure_o=$attachment->getEnclosure();
472 $enclosure = array();
473 $enclosure['url'] = $enclosure_o->url;
474 $enclosure['mimetype'] = $enclosure_o->mimetype;
475 $enclosure['size'] = $enclosure_o->size;
476 $enclosures[] = $enclosure;
480 if (!empty($enclosures)) {
481 $entry['enclosures'] = $enclosures;
485 $tag = new Notice_tag();
486 $tag->notice_id = $notice->id;
488 $entry['tags']=array();
489 while ($tag->fetch()) {
490 $entry['tags'][]=$tag->tag;
496 $entry['description'] = $entry['content'];
497 $entry['pubDate'] = common_date_rfc2822($notice->created);
498 $entry['guid'] = $entry['link'];
500 if (isset($notice->lat) && isset($notice->lon)) {
501 // This is the format that GeoJSON expects stuff to be in.
502 // showGeoRSS() below uses it for XML output, so we reuse it
503 $entry['geo'] = array('type' => 'Point',
504 'coordinates' => array((float) $notice->lat,
505 (float) $notice->lon));
507 $entry['geo'] = null;
513 function twitterRelationshipArray($source, $target)
515 $relationship = array();
517 $relationship['source'] =
518 $this->relationshipDetailsArray($source, $target);
519 $relationship['target'] =
520 $this->relationshipDetailsArray($target, $source);
522 return array('relationship' => $relationship);
525 function relationshipDetailsArray($source, $target)
529 $details['screen_name'] = $source->nickname;
530 $details['followed_by'] = $target->isSubscribed($source);
531 $details['following'] = $source->isSubscribed($target);
533 $notifications = false;
535 if ($source->isSubscribed($target)) {
537 $sub = Subscription::pkeyGet(array('subscriber' =>
538 $source->id, 'subscribed' => $target->id));
541 $notifications = ($sub->jabber || $sub->sms);
545 $details['notifications_enabled'] = $notifications;
546 $details['blocking'] = $source->hasBlocked($target);
547 $details['id'] = $source->id;
552 function showTwitterXmlRelationship($relationship)
554 $this->elementStart('relationship');
556 foreach($relationship as $element => $value) {
557 if ($element == 'source' || $element == 'target') {
558 $this->elementStart($element);
559 $this->showXmlRelationshipDetails($value);
560 $this->elementEnd($element);
564 $this->elementEnd('relationship');
567 function showXmlRelationshipDetails($details)
569 foreach($details as $element => $value) {
570 $this->element($element, null, $value);
574 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
578 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
580 $this->elementStart($tag, $attrs);
581 foreach($twitter_status as $element => $value) {
584 $this->showTwitterXmlUser($twitter_status['user']);
587 $this->element($element, null, common_xml_safe_str($value));
590 $this->showXmlAttachments($twitter_status['attachments']);
593 $this->showGeoXML($value);
595 case 'retweeted_status':
596 $this->showTwitterXmlStatus($value, 'retweeted_status');
599 $this->element($element, null, $value);
602 $this->elementEnd($tag);
605 function showTwitterXmlGroup($twitter_group)
607 $this->elementStart('group');
608 foreach($twitter_group as $element => $value) {
609 $this->element($element, null, $value);
611 $this->elementEnd('group');
614 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
618 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
620 $this->elementStart($role, $attrs);
621 foreach($twitter_user as $element => $value) {
622 if ($element == 'status') {
623 $this->showTwitterXmlStatus($twitter_user['status']);
625 $this->element($element, null, $value);
628 $this->elementEnd($role);
631 function showXmlAttachments($attachments) {
632 if (!empty($attachments)) {
633 $this->elementStart('attachments', array('type' => 'array'));
634 foreach ($attachments as $attachment) {
636 $attrs['url'] = $attachment['url'];
637 $attrs['mimetype'] = $attachment['mimetype'];
638 $attrs['size'] = $attachment['size'];
639 $this->element('enclosure', $attrs, '');
641 $this->elementEnd('attachments');
645 function showGeoXML($geo)
649 $this->element('geo');
651 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
652 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
653 $this->elementEnd('geo');
657 function showGeoRSS($geo)
663 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
668 function showTwitterRssItem($entry)
670 $this->elementStart('item');
671 $this->element('title', null, $entry['title']);
672 $this->element('description', null, $entry['description']);
673 $this->element('pubDate', null, $entry['pubDate']);
674 $this->element('guid', null, $entry['guid']);
675 $this->element('link', null, $entry['link']);
677 # RSS only supports 1 enclosure per item
678 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
679 $enclosure = $entry['enclosures'][0];
680 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
683 if(array_key_exists('tags', $entry)){
684 foreach($entry['tags'] as $tag){
685 $this->element('category', null,$tag);
689 $this->showGeoRSS($entry['geo']);
690 $this->elementEnd('item');
693 function showJsonObjects($objects)
695 print(json_encode($objects));
698 function showSingleXmlStatus($notice)
700 $this->initDocument('xml');
701 $twitter_status = $this->twitterStatusArray($notice);
702 $this->showTwitterXmlStatus($twitter_status, 'status', true);
703 $this->endDocument('xml');
706 function show_single_json_status($notice)
708 $this->initDocument('json');
709 $status = $this->twitterStatusArray($notice);
710 $this->showJsonObjects($status);
711 $this->endDocument('json');
714 function showXmlTimeline($notice)
717 $this->initDocument('xml');
718 $this->elementStart('statuses', array('type' => 'array',
719 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
721 if (is_array($notice)) {
722 foreach ($notice as $n) {
723 $twitter_status = $this->twitterStatusArray($n);
724 $this->showTwitterXmlStatus($twitter_status);
727 while ($notice->fetch()) {
728 $twitter_status = $this->twitterStatusArray($notice);
729 $this->showTwitterXmlStatus($twitter_status);
733 $this->elementEnd('statuses');
734 $this->endDocument('xml');
737 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
740 $this->initDocument('rss');
742 $this->element('title', null, $title);
743 $this->element('link', null, $link);
745 if (!is_null($self)) {
749 'type' => 'application/rss+xml',
756 if (!is_null($suplink)) {
757 // For FriendFeed's SUP protocol
758 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
759 'rel' => 'http://api.friendfeed.com/2008/03#sup',
761 'type' => 'application/json'));
764 if (!is_null($logo)) {
765 $this->elementStart('image');
766 $this->element('link', null, $link);
767 $this->element('title', null, $title);
768 $this->element('url', null, $logo);
769 $this->elementEnd('image');
772 $this->element('description', null, $subtitle);
773 $this->element('language', null, 'en-us');
774 $this->element('ttl', null, '40');
776 if (is_array($notice)) {
777 foreach ($notice as $n) {
778 $entry = $this->twitterRssEntryArray($n);
779 $this->showTwitterRssItem($entry);
782 while ($notice->fetch()) {
783 $entry = $this->twitterRssEntryArray($notice);
784 $this->showTwitterRssItem($entry);
788 $this->endTwitterRss();
791 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
794 $this->initDocument('atom');
796 $this->element('title', null, $title);
797 $this->element('id', null, $id);
798 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
800 if (!is_null($logo)) {
801 $this->element('logo',null,$logo);
804 if (!is_null($suplink)) {
805 # For FriendFeed's SUP protocol
806 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
808 'type' => 'application/json'));
811 if (!is_null($selfuri)) {
812 $this->element('link', array('href' => $selfuri,
813 'rel' => 'self', 'type' => 'application/atom+xml'), null);
816 $this->element('updated', null, common_date_iso8601('now'));
817 $this->element('subtitle', null, $subtitle);
819 if (is_array($notice)) {
820 foreach ($notice as $n) {
821 $this->raw($n->asAtomEntry());
824 while ($notice->fetch()) {
825 $this->raw($notice->asAtomEntry());
829 $this->endDocument('atom');
833 function showRssGroups($group, $title, $link, $subtitle)
836 $this->initDocument('rss');
838 $this->element('title', null, $title);
839 $this->element('link', null, $link);
840 $this->element('description', null, $subtitle);
841 $this->element('language', null, 'en-us');
842 $this->element('ttl', null, '40');
844 if (is_array($group)) {
845 foreach ($group as $g) {
846 $twitter_group = $this->twitterRssGroupArray($g);
847 $this->showTwitterRssItem($twitter_group);
850 while ($group->fetch()) {
851 $twitter_group = $this->twitterRssGroupArray($group);
852 $this->showTwitterRssItem($twitter_group);
856 $this->endTwitterRss();
859 function showTwitterAtomEntry($entry)
861 $this->elementStart('entry');
862 $this->element('title', null, common_xml_safe_str($entry['title']));
865 array('type' => 'html'),
866 common_xml_safe_str($entry['content'])
868 $this->element('id', null, $entry['id']);
869 $this->element('published', null, $entry['published']);
870 $this->element('updated', null, $entry['updated']);
871 $this->element('link', array('type' => 'text/html',
872 'href' => $entry['link'],
873 'rel' => 'alternate'));
874 $this->element('link', array('type' => $entry['avatar-type'],
875 'href' => $entry['avatar'],
877 $this->elementStart('author');
879 $this->element('name', null, $entry['author-name']);
880 $this->element('uri', null, $entry['author-uri']);
882 $this->elementEnd('author');
883 $this->elementEnd('entry');
886 function showXmlDirectMessage($dm, $namespaces=false)
890 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
892 $this->elementStart('direct_message', $attrs);
893 foreach($dm as $element => $value) {
897 $this->showTwitterXmlUser($value, $element);
900 $this->element($element, null, common_xml_safe_str($value));
903 $this->element($element, null, $value);
907 $this->elementEnd('direct_message');
910 function directMessageArray($message)
914 $from_profile = $message->getFrom();
915 $to_profile = $message->getTo();
917 $dmsg['id'] = $message->id;
918 $dmsg['sender_id'] = $message->from_profile;
919 $dmsg['text'] = trim($message->content);
920 $dmsg['recipient_id'] = $message->to_profile;
921 $dmsg['created_at'] = $this->dateTwitter($message->created);
922 $dmsg['sender_screen_name'] = $from_profile->nickname;
923 $dmsg['recipient_screen_name'] = $to_profile->nickname;
924 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
925 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
930 function rssDirectMessageArray($message)
934 $from = $message->getFrom();
936 $entry['title'] = sprintf('Message from %1$s to %2$s',
937 $from->nickname, $message->getTo()->nickname);
939 $entry['content'] = common_xml_safe_str($message->rendered);
940 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
941 $entry['published'] = common_date_iso8601($message->created);
943 $taguribase = TagURI::base();
945 $entry['id'] = "tag:$taguribase:$entry[link]";
946 $entry['updated'] = $entry['published'];
948 $entry['author-name'] = $from->getBestName();
949 $entry['author-uri'] = $from->homepage;
951 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
953 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
954 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
958 $entry['description'] = $entry['content'];
959 $entry['pubDate'] = common_date_rfc2822($message->created);
960 $entry['guid'] = $entry['link'];
965 function showSingleXmlDirectMessage($message)
967 $this->initDocument('xml');
968 $dmsg = $this->directMessageArray($message);
969 $this->showXmlDirectMessage($dmsg, true);
970 $this->endDocument('xml');
973 function showSingleJsonDirectMessage($message)
975 $this->initDocument('json');
976 $dmsg = $this->directMessageArray($message);
977 $this->showJsonObjects($dmsg);
978 $this->endDocument('json');
981 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
984 $this->initDocument('atom');
986 $this->element('title', null, common_xml_safe_str($title));
987 $this->element('id', null, $id);
988 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
990 if (!is_null($selfuri)) {
991 $this->element('link', array('href' => $selfuri,
992 'rel' => 'self', 'type' => 'application/atom+xml'), null);
995 $this->element('updated', null, common_date_iso8601('now'));
996 $this->element('subtitle', null, common_xml_safe_str($subtitle));
998 if (is_array($group)) {
999 foreach ($group as $g) {
1000 $this->raw($g->asAtomEntry());
1003 while ($group->fetch()) {
1004 $this->raw($group->asAtomEntry());
1008 $this->endDocument('atom');
1012 function showJsonTimeline($notice)
1015 $this->initDocument('json');
1017 $statuses = array();
1019 if (is_array($notice)) {
1020 foreach ($notice as $n) {
1021 $twitter_status = $this->twitterStatusArray($n);
1022 array_push($statuses, $twitter_status);
1025 while ($notice->fetch()) {
1026 $twitter_status = $this->twitterStatusArray($notice);
1027 array_push($statuses, $twitter_status);
1031 $this->showJsonObjects($statuses);
1033 $this->endDocument('json');
1036 function showJsonGroups($group)
1039 $this->initDocument('json');
1043 if (is_array($group)) {
1044 foreach ($group as $g) {
1045 $twitter_group = $this->twitterGroupArray($g);
1046 array_push($groups, $twitter_group);
1049 while ($group->fetch()) {
1050 $twitter_group = $this->twitterGroupArray($group);
1051 array_push($groups, $twitter_group);
1055 $this->showJsonObjects($groups);
1057 $this->endDocument('json');
1060 function showXmlGroups($group)
1063 $this->initDocument('xml');
1064 $this->elementStart('groups', array('type' => 'array'));
1066 if (is_array($group)) {
1067 foreach ($group as $g) {
1068 $twitter_group = $this->twitterGroupArray($g);
1069 $this->showTwitterXmlGroup($twitter_group);
1072 while ($group->fetch()) {
1073 $twitter_group = $this->twitterGroupArray($group);
1074 $this->showTwitterXmlGroup($twitter_group);
1078 $this->elementEnd('groups');
1079 $this->endDocument('xml');
1082 function showTwitterXmlUsers($user)
1085 $this->initDocument('xml');
1086 $this->elementStart('users', array('type' => 'array',
1087 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1089 if (is_array($user)) {
1090 foreach ($user as $u) {
1091 $twitter_user = $this->twitterUserArray($u);
1092 $this->showTwitterXmlUser($twitter_user);
1095 while ($user->fetch()) {
1096 $twitter_user = $this->twitterUserArray($user);
1097 $this->showTwitterXmlUser($twitter_user);
1101 $this->elementEnd('users');
1102 $this->endDocument('xml');
1105 function showJsonUsers($user)
1108 $this->initDocument('json');
1112 if (is_array($user)) {
1113 foreach ($user as $u) {
1114 $twitter_user = $this->twitterUserArray($u);
1115 array_push($users, $twitter_user);
1118 while ($user->fetch()) {
1119 $twitter_user = $this->twitterUserArray($user);
1120 array_push($users, $twitter_user);
1124 $this->showJsonObjects($users);
1126 $this->endDocument('json');
1129 function showSingleJsonGroup($group)
1131 $this->initDocument('json');
1132 $twitter_group = $this->twitterGroupArray($group);
1133 $this->showJsonObjects($twitter_group);
1134 $this->endDocument('json');
1137 function showSingleXmlGroup($group)
1139 $this->initDocument('xml');
1140 $twitter_group = $this->twitterGroupArray($group);
1141 $this->showTwitterXmlGroup($twitter_group);
1142 $this->endDocument('xml');
1145 function dateTwitter($dt)
1147 $dateStr = date('d F Y H:i:s', strtotime($dt));
1148 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1149 $d->setTimezone(new DateTimeZone(common_timezone()));
1150 return $d->format('D M d H:i:s O Y');
1153 function initDocument($type='xml')
1157 header('Content-Type: application/xml; charset=utf-8');
1161 header('Content-Type: application/json; charset=utf-8');
1163 // Check for JSONP callback
1164 $callback = $this->arg('callback');
1166 print $callback . '(';
1170 header("Content-Type: application/rss+xml; charset=utf-8");
1171 $this->initTwitterRss();
1174 header('Content-Type: application/atom+xml; charset=utf-8');
1175 $this->initTwitterAtom();
1178 // TRANS: Client error on an API request with an unsupported data format.
1179 $this->clientError(_('Not a supported data format.'));
1186 function endDocument($type='xml')
1194 // Check for JSONP callback
1195 $callback = $this->arg('callback');
1201 $this->endTwitterRss();
1204 $this->endTwitterRss();
1207 // TRANS: Client error on an API request with an unsupported data format.
1208 $this->clientError(_('Not a supported data format.'));
1214 function clientError($msg, $code = 400, $format = 'xml')
1216 $action = $this->trimmed('action');
1218 common_debug("User error '$code' on '$action': $msg", __FILE__);
1220 if (!array_key_exists($code, ClientErrorAction::$status)) {
1224 $status_string = ClientErrorAction::$status[$code];
1226 header('HTTP/1.1 '.$code.' '.$status_string);
1228 if ($format == 'xml') {
1229 $this->initDocument('xml');
1230 $this->elementStart('hash');
1231 $this->element('error', null, $msg);
1232 $this->element('request', null, $_SERVER['REQUEST_URI']);
1233 $this->elementEnd('hash');
1234 $this->endDocument('xml');
1235 } elseif ($format == 'json'){
1236 $this->initDocument('json');
1237 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1238 print(json_encode($error_array));
1239 $this->endDocument('json');
1242 // If user didn't request a useful format, throw a regular client error
1243 throw new ClientException($msg, $code);
1247 function serverError($msg, $code = 500, $content_type = 'xml')
1249 $action = $this->trimmed('action');
1251 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1253 if (!array_key_exists($code, ServerErrorAction::$status)) {
1257 $status_string = ServerErrorAction::$status[$code];
1259 header('HTTP/1.1 '.$code.' '.$status_string);
1261 if ($content_type == 'xml') {
1262 $this->initDocument('xml');
1263 $this->elementStart('hash');
1264 $this->element('error', null, $msg);
1265 $this->element('request', null, $_SERVER['REQUEST_URI']);
1266 $this->elementEnd('hash');
1267 $this->endDocument('xml');
1269 $this->initDocument('json');
1270 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1271 print(json_encode($error_array));
1272 $this->endDocument('json');
1276 function initTwitterRss()
1279 $this->elementStart(
1283 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1284 'xmlns:georss' => 'http://www.georss.org/georss'
1287 $this->elementStart('channel');
1288 Event::handle('StartApiRss', array($this));
1291 function endTwitterRss()
1293 $this->elementEnd('channel');
1294 $this->elementEnd('rss');
1298 function initTwitterAtom()
1301 // FIXME: don't hardcode the language here!
1302 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1303 'xml:lang' => 'en-US',
1304 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1307 function endTwitterAtom()
1309 $this->elementEnd('feed');
1313 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1315 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1316 switch ($content_type) {
1318 $this->showTwitterXmlUser($profile_array);
1321 $this->showJsonObjects($profile_array);
1324 // TRANS: Client error on an API request with an unsupported data format.
1325 $this->clientError(_('Not a supported data format.'));
1331 function getTargetUser($id)
1335 // Twitter supports these other ways of passing the user ID
1336 if (is_numeric($this->arg('id'))) {
1337 return User::staticGet($this->arg('id'));
1338 } else if ($this->arg('id')) {
1339 $nickname = common_canonical_nickname($this->arg('id'));
1340 return User::staticGet('nickname', $nickname);
1341 } else if ($this->arg('user_id')) {
1342 // This is to ensure that a non-numeric user_id still
1343 // overrides screen_name even if it doesn't get used
1344 if (is_numeric($this->arg('user_id'))) {
1345 return User::staticGet('id', $this->arg('user_id'));
1347 } else if ($this->arg('screen_name')) {
1348 $nickname = common_canonical_nickname($this->arg('screen_name'));
1349 return User::staticGet('nickname', $nickname);
1351 // Fall back to trying the currently authenticated user
1352 return $this->auth_user;
1355 } else if (is_numeric($id)) {
1356 return User::staticGet($id);
1358 $nickname = common_canonical_nickname($id);
1359 return User::staticGet('nickname', $nickname);
1363 function getTargetGroup($id)
1366 if (is_numeric($this->arg('id'))) {
1367 return User_group::staticGet($this->arg('id'));
1368 } else if ($this->arg('id')) {
1369 $nickname = common_canonical_nickname($this->arg('id'));
1370 $local = Local_group::staticGet('nickname', $nickname);
1371 if (empty($local)) {
1374 return User_group::staticGet('id', $local->id);
1376 } else if ($this->arg('group_id')) {
1377 // This is to ensure that a non-numeric user_id still
1378 // overrides screen_name even if it doesn't get used
1379 if (is_numeric($this->arg('group_id'))) {
1380 return User_group::staticGet('id', $this->arg('group_id'));
1382 } else if ($this->arg('group_name')) {
1383 $nickname = common_canonical_nickname($this->arg('group_name'));
1384 $local = Local_group::staticGet('nickname', $nickname);
1385 if (empty($local)) {
1388 return User_group::staticGet('id', $local->group_id);
1392 } else if (is_numeric($id)) {
1393 return User_group::staticGet($id);
1395 $nickname = common_canonical_nickname($id);
1396 $local = Local_group::staticGet('nickname', $nickname);
1397 if (empty($local)) {
1400 return User_group::staticGet('id', $local->group_id);
1406 * Returns query argument or default value if not found. Certain
1407 * parameters used throughout the API are lightly scrubbed and
1408 * bounds checked. This overrides Action::arg().
1410 * @param string $key requested argument
1411 * @param string $def default value to return if $key is not provided
1415 function arg($key, $def=null)
1418 // XXX: Do even more input validation/scrubbing?
1420 if (array_key_exists($key, $this->args)) {
1423 $page = (int)$this->args['page'];
1424 return ($page < 1) ? 1 : $page;
1426 $count = (int)$this->args['count'];
1429 } elseif ($count > 200) {
1435 $since_id = (int)$this->args['since_id'];
1436 return ($since_id < 1) ? 0 : $since_id;
1438 $max_id = (int)$this->args['max_id'];
1439 return ($max_id < 1) ? 0 : $max_id;
1441 return parent::arg($key, $def);
1449 * Calculate the complete URI that called up this action. Used for
1450 * Atom rel="self" links. Warning: this is funky.
1452 * @return string URL a URL suitable for rel="self" Atom links
1454 function getSelfUri()
1456 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1458 $id = $this->arg('id');
1459 $aargs = array('format' => $this->format);
1464 $tag = $this->arg('tag');
1466 $aargs['tag'] = $tag;
1469 parse_str($_SERVER['QUERY_STRING'], $params);
1471 if (!empty($params)) {
1472 unset($params['p']);
1473 $pstring = http_build_query($params);
1476 $uri = common_local_url($action, $aargs);
1478 if (!empty($pstring)) {
1479 $uri .= '?' . $pstring;