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 // StatusNet-specific
302 $twitter_user['statusnet:profile_url'] = $profile->profileurl;
304 return $twitter_user;
307 function twitterStatusArray($notice, $include_user=true)
309 $base = $this->twitterSimpleStatusArray($notice, $include_user);
311 if (!empty($notice->repeat_of)) {
312 $original = Notice::staticGet('id', $notice->repeat_of);
313 if (!empty($original)) {
314 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
315 $base['retweeted_status'] = $original_array;
322 function twitterSimpleStatusArray($notice, $include_user=true)
324 $profile = $notice->getProfile();
326 $twitter_status = array();
327 $twitter_status['text'] = $notice->content;
328 $twitter_status['truncated'] = false; # Not possible on StatusNet
329 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
330 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
331 intval($notice->reply_to) : null;
335 $ns = $notice->getSource();
337 if (!empty($ns->name) && !empty($ns->url)) {
338 $source = '<a href="'
339 . htmlspecialchars($ns->url)
340 . '" rel="nofollow">'
341 . htmlspecialchars($ns->name)
348 $twitter_status['source'] = $source;
349 $twitter_status['id'] = intval($notice->id);
351 $replier_profile = null;
353 if ($notice->reply_to) {
354 $reply = Notice::staticGet(intval($notice->reply_to));
356 $replier_profile = $reply->getProfile();
360 $twitter_status['in_reply_to_user_id'] =
361 ($replier_profile) ? intval($replier_profile->id) : null;
362 $twitter_status['in_reply_to_screen_name'] =
363 ($replier_profile) ? $replier_profile->nickname : null;
365 if (isset($notice->lat) && isset($notice->lon)) {
366 // This is the format that GeoJSON expects stuff to be in
367 $twitter_status['geo'] = array('type' => 'Point',
368 'coordinates' => array((float) $notice->lat,
369 (float) $notice->lon));
371 $twitter_status['geo'] = null;
374 if (isset($this->auth_user)) {
375 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
377 $twitter_status['favorited'] = false;
381 $attachments = $notice->attachments();
383 if (!empty($attachments)) {
385 $twitter_status['attachments'] = array();
387 foreach ($attachments as $attachment) {
388 $enclosure_o=$attachment->getEnclosure();
390 $enclosure = array();
391 $enclosure['url'] = $enclosure_o->url;
392 $enclosure['mimetype'] = $enclosure_o->mimetype;
393 $enclosure['size'] = $enclosure_o->size;
394 $twitter_status['attachments'][] = $enclosure;
399 if ($include_user && $profile) {
400 # Don't get notice (recursive!)
401 $twitter_user = $this->twitterUserArray($profile, false);
402 $twitter_status['user'] = $twitter_user;
405 // StatusNet-specific
407 $twitter_status['statusnet:html'] = $notice->rendered;
409 return $twitter_status;
412 function twitterGroupArray($group)
414 $twitter_group=array();
415 $twitter_group['id']=$group->id;
416 $twitter_group['url']=$group->permalink();
417 $twitter_group['nickname']=$group->nickname;
418 $twitter_group['fullname']=$group->fullname;
419 $twitter_group['original_logo']=$group->original_logo;
420 $twitter_group['homepage_logo']=$group->homepage_logo;
421 $twitter_group['stream_logo']=$group->stream_logo;
422 $twitter_group['mini_logo']=$group->mini_logo;
423 $twitter_group['homepage']=$group->homepage;
424 $twitter_group['description']=$group->description;
425 $twitter_group['location']=$group->location;
426 $twitter_group['created']=$this->dateTwitter($group->created);
427 $twitter_group['modified']=$this->dateTwitter($group->modified);
428 return $twitter_group;
431 function twitterRssGroupArray($group)
434 $entry['content']=$group->description;
435 $entry['title']=$group->nickname;
436 $entry['link']=$group->permalink();
437 $entry['published']=common_date_iso8601($group->created);
438 $entry['updated']==common_date_iso8601($group->modified);
439 $taguribase = common_config('integration', 'groupuri');
440 $entry['id'] = "group:$groupuribase:$entry[link]";
442 $entry['description'] = $entry['content'];
443 $entry['pubDate'] = common_date_rfc2822($group->created);
444 $entry['guid'] = $entry['link'];
449 function twitterRssEntryArray($notice)
451 $profile = $notice->getProfile();
454 // We trim() to avoid extraneous whitespace in the output
456 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
457 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
458 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
459 $entry['published'] = common_date_iso8601($notice->created);
461 $taguribase = TagURI::base();
462 $entry['id'] = "tag:$taguribase:$entry[link]";
464 $entry['updated'] = $entry['published'];
465 $entry['author'] = $profile->getBestName();
468 $attachments = $notice->attachments();
469 $enclosures = array();
471 foreach ($attachments as $attachment) {
472 $enclosure_o=$attachment->getEnclosure();
474 $enclosure = array();
475 $enclosure['url'] = $enclosure_o->url;
476 $enclosure['mimetype'] = $enclosure_o->mimetype;
477 $enclosure['size'] = $enclosure_o->size;
478 $enclosures[] = $enclosure;
482 if (!empty($enclosures)) {
483 $entry['enclosures'] = $enclosures;
487 $tag = new Notice_tag();
488 $tag->notice_id = $notice->id;
490 $entry['tags']=array();
491 while ($tag->fetch()) {
492 $entry['tags'][]=$tag->tag;
498 $entry['description'] = $entry['content'];
499 $entry['pubDate'] = common_date_rfc2822($notice->created);
500 $entry['guid'] = $entry['link'];
502 if (isset($notice->lat) && isset($notice->lon)) {
503 // This is the format that GeoJSON expects stuff to be in.
504 // showGeoRSS() below uses it for XML output, so we reuse it
505 $entry['geo'] = array('type' => 'Point',
506 'coordinates' => array((float) $notice->lat,
507 (float) $notice->lon));
509 $entry['geo'] = null;
515 function twitterRelationshipArray($source, $target)
517 $relationship = array();
519 $relationship['source'] =
520 $this->relationshipDetailsArray($source, $target);
521 $relationship['target'] =
522 $this->relationshipDetailsArray($target, $source);
524 return array('relationship' => $relationship);
527 function relationshipDetailsArray($source, $target)
531 $details['screen_name'] = $source->nickname;
532 $details['followed_by'] = $target->isSubscribed($source);
533 $details['following'] = $source->isSubscribed($target);
535 $notifications = false;
537 if ($source->isSubscribed($target)) {
539 $sub = Subscription::pkeyGet(array('subscriber' =>
540 $source->id, 'subscribed' => $target->id));
543 $notifications = ($sub->jabber || $sub->sms);
547 $details['notifications_enabled'] = $notifications;
548 $details['blocking'] = $source->hasBlocked($target);
549 $details['id'] = $source->id;
554 function showTwitterXmlRelationship($relationship)
556 $this->elementStart('relationship');
558 foreach($relationship as $element => $value) {
559 if ($element == 'source' || $element == 'target') {
560 $this->elementStart($element);
561 $this->showXmlRelationshipDetails($value);
562 $this->elementEnd($element);
566 $this->elementEnd('relationship');
569 function showXmlRelationshipDetails($details)
571 foreach($details as $element => $value) {
572 $this->element($element, null, $value);
576 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
580 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
582 $this->elementStart($tag, $attrs);
583 foreach($twitter_status as $element => $value) {
586 $this->showTwitterXmlUser($twitter_status['user']);
589 $this->element($element, null, common_xml_safe_str($value));
592 $this->showXmlAttachments($twitter_status['attachments']);
595 $this->showGeoXML($value);
597 case 'retweeted_status':
598 $this->showTwitterXmlStatus($value, 'retweeted_status');
601 $this->element($element, null, $value);
604 $this->elementEnd($tag);
607 function showTwitterXmlGroup($twitter_group)
609 $this->elementStart('group');
610 foreach($twitter_group as $element => $value) {
611 $this->element($element, null, $value);
613 $this->elementEnd('group');
616 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
620 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
622 $this->elementStart($role, $attrs);
623 foreach($twitter_user as $element => $value) {
624 if ($element == 'status') {
625 $this->showTwitterXmlStatus($twitter_user['status']);
627 $this->element($element, null, $value);
630 $this->elementEnd($role);
633 function showXmlAttachments($attachments) {
634 if (!empty($attachments)) {
635 $this->elementStart('attachments', array('type' => 'array'));
636 foreach ($attachments as $attachment) {
638 $attrs['url'] = $attachment['url'];
639 $attrs['mimetype'] = $attachment['mimetype'];
640 $attrs['size'] = $attachment['size'];
641 $this->element('enclosure', $attrs, '');
643 $this->elementEnd('attachments');
647 function showGeoXML($geo)
651 $this->element('geo');
653 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
654 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
655 $this->elementEnd('geo');
659 function showGeoRSS($geo)
665 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
670 function showTwitterRssItem($entry)
672 $this->elementStart('item');
673 $this->element('title', null, $entry['title']);
674 $this->element('description', null, $entry['description']);
675 $this->element('pubDate', null, $entry['pubDate']);
676 $this->element('guid', null, $entry['guid']);
677 $this->element('link', null, $entry['link']);
679 # RSS only supports 1 enclosure per item
680 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
681 $enclosure = $entry['enclosures'][0];
682 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
685 if(array_key_exists('tags', $entry)){
686 foreach($entry['tags'] as $tag){
687 $this->element('category', null,$tag);
691 $this->showGeoRSS($entry['geo']);
692 $this->elementEnd('item');
695 function showJsonObjects($objects)
697 print(json_encode($objects));
700 function showSingleXmlStatus($notice)
702 $this->initDocument('xml');
703 $twitter_status = $this->twitterStatusArray($notice);
704 $this->showTwitterXmlStatus($twitter_status, 'status', true);
705 $this->endDocument('xml');
708 function show_single_json_status($notice)
710 $this->initDocument('json');
711 $status = $this->twitterStatusArray($notice);
712 $this->showJsonObjects($status);
713 $this->endDocument('json');
716 function showXmlTimeline($notice)
719 $this->initDocument('xml');
720 $this->elementStart('statuses', array('type' => 'array',
721 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
723 if (is_array($notice)) {
724 foreach ($notice as $n) {
725 $twitter_status = $this->twitterStatusArray($n);
726 $this->showTwitterXmlStatus($twitter_status);
729 while ($notice->fetch()) {
730 $twitter_status = $this->twitterStatusArray($notice);
731 $this->showTwitterXmlStatus($twitter_status);
735 $this->elementEnd('statuses');
736 $this->endDocument('xml');
739 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
742 $this->initDocument('rss');
744 $this->element('title', null, $title);
745 $this->element('link', null, $link);
747 if (!is_null($self)) {
751 'type' => 'application/rss+xml',
758 if (!is_null($suplink)) {
759 // For FriendFeed's SUP protocol
760 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
761 'rel' => 'http://api.friendfeed.com/2008/03#sup',
763 'type' => 'application/json'));
766 if (!is_null($logo)) {
767 $this->elementStart('image');
768 $this->element('link', null, $link);
769 $this->element('title', null, $title);
770 $this->element('url', null, $logo);
771 $this->elementEnd('image');
774 $this->element('description', null, $subtitle);
775 $this->element('language', null, 'en-us');
776 $this->element('ttl', null, '40');
778 if (is_array($notice)) {
779 foreach ($notice as $n) {
780 $entry = $this->twitterRssEntryArray($n);
781 $this->showTwitterRssItem($entry);
784 while ($notice->fetch()) {
785 $entry = $this->twitterRssEntryArray($notice);
786 $this->showTwitterRssItem($entry);
790 $this->endTwitterRss();
793 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
796 $this->initDocument('atom');
798 $this->element('title', null, $title);
799 $this->element('id', null, $id);
800 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
802 if (!is_null($logo)) {
803 $this->element('logo',null,$logo);
806 if (!is_null($suplink)) {
807 # For FriendFeed's SUP protocol
808 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
810 'type' => 'application/json'));
813 if (!is_null($selfuri)) {
814 $this->element('link', array('href' => $selfuri,
815 'rel' => 'self', 'type' => 'application/atom+xml'), null);
818 $this->element('updated', null, common_date_iso8601('now'));
819 $this->element('subtitle', null, $subtitle);
821 if (is_array($notice)) {
822 foreach ($notice as $n) {
823 $this->raw($n->asAtomEntry());
826 while ($notice->fetch()) {
827 $this->raw($notice->asAtomEntry());
831 $this->endDocument('atom');
835 function showRssGroups($group, $title, $link, $subtitle)
838 $this->initDocument('rss');
840 $this->element('title', null, $title);
841 $this->element('link', null, $link);
842 $this->element('description', null, $subtitle);
843 $this->element('language', null, 'en-us');
844 $this->element('ttl', null, '40');
846 if (is_array($group)) {
847 foreach ($group as $g) {
848 $twitter_group = $this->twitterRssGroupArray($g);
849 $this->showTwitterRssItem($twitter_group);
852 while ($group->fetch()) {
853 $twitter_group = $this->twitterRssGroupArray($group);
854 $this->showTwitterRssItem($twitter_group);
858 $this->endTwitterRss();
861 function showTwitterAtomEntry($entry)
863 $this->elementStart('entry');
864 $this->element('title', null, common_xml_safe_str($entry['title']));
867 array('type' => 'html'),
868 common_xml_safe_str($entry['content'])
870 $this->element('id', null, $entry['id']);
871 $this->element('published', null, $entry['published']);
872 $this->element('updated', null, $entry['updated']);
873 $this->element('link', array('type' => 'text/html',
874 'href' => $entry['link'],
875 'rel' => 'alternate'));
876 $this->element('link', array('type' => $entry['avatar-type'],
877 'href' => $entry['avatar'],
879 $this->elementStart('author');
881 $this->element('name', null, $entry['author-name']);
882 $this->element('uri', null, $entry['author-uri']);
884 $this->elementEnd('author');
885 $this->elementEnd('entry');
888 function showXmlDirectMessage($dm, $namespaces=false)
892 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
894 $this->elementStart('direct_message', $attrs);
895 foreach($dm as $element => $value) {
899 $this->showTwitterXmlUser($value, $element);
902 $this->element($element, null, common_xml_safe_str($value));
905 $this->element($element, null, $value);
909 $this->elementEnd('direct_message');
912 function directMessageArray($message)
916 $from_profile = $message->getFrom();
917 $to_profile = $message->getTo();
919 $dmsg['id'] = $message->id;
920 $dmsg['sender_id'] = $message->from_profile;
921 $dmsg['text'] = trim($message->content);
922 $dmsg['recipient_id'] = $message->to_profile;
923 $dmsg['created_at'] = $this->dateTwitter($message->created);
924 $dmsg['sender_screen_name'] = $from_profile->nickname;
925 $dmsg['recipient_screen_name'] = $to_profile->nickname;
926 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
927 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
932 function rssDirectMessageArray($message)
936 $from = $message->getFrom();
938 $entry['title'] = sprintf('Message from %1$s to %2$s',
939 $from->nickname, $message->getTo()->nickname);
941 $entry['content'] = common_xml_safe_str($message->rendered);
942 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
943 $entry['published'] = common_date_iso8601($message->created);
945 $taguribase = TagURI::base();
947 $entry['id'] = "tag:$taguribase:$entry[link]";
948 $entry['updated'] = $entry['published'];
950 $entry['author-name'] = $from->getBestName();
951 $entry['author-uri'] = $from->homepage;
953 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
955 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
956 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
960 $entry['description'] = $entry['content'];
961 $entry['pubDate'] = common_date_rfc2822($message->created);
962 $entry['guid'] = $entry['link'];
967 function showSingleXmlDirectMessage($message)
969 $this->initDocument('xml');
970 $dmsg = $this->directMessageArray($message);
971 $this->showXmlDirectMessage($dmsg, true);
972 $this->endDocument('xml');
975 function showSingleJsonDirectMessage($message)
977 $this->initDocument('json');
978 $dmsg = $this->directMessageArray($message);
979 $this->showJsonObjects($dmsg);
980 $this->endDocument('json');
983 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
986 $this->initDocument('atom');
988 $this->element('title', null, common_xml_safe_str($title));
989 $this->element('id', null, $id);
990 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
992 if (!is_null($selfuri)) {
993 $this->element('link', array('href' => $selfuri,
994 'rel' => 'self', 'type' => 'application/atom+xml'), null);
997 $this->element('updated', null, common_date_iso8601('now'));
998 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1000 if (is_array($group)) {
1001 foreach ($group as $g) {
1002 $this->raw($g->asAtomEntry());
1005 while ($group->fetch()) {
1006 $this->raw($group->asAtomEntry());
1010 $this->endDocument('atom');
1014 function showJsonTimeline($notice)
1017 $this->initDocument('json');
1019 $statuses = array();
1021 if (is_array($notice)) {
1022 foreach ($notice as $n) {
1023 $twitter_status = $this->twitterStatusArray($n);
1024 array_push($statuses, $twitter_status);
1027 while ($notice->fetch()) {
1028 $twitter_status = $this->twitterStatusArray($notice);
1029 array_push($statuses, $twitter_status);
1033 $this->showJsonObjects($statuses);
1035 $this->endDocument('json');
1038 function showJsonGroups($group)
1041 $this->initDocument('json');
1045 if (is_array($group)) {
1046 foreach ($group as $g) {
1047 $twitter_group = $this->twitterGroupArray($g);
1048 array_push($groups, $twitter_group);
1051 while ($group->fetch()) {
1052 $twitter_group = $this->twitterGroupArray($group);
1053 array_push($groups, $twitter_group);
1057 $this->showJsonObjects($groups);
1059 $this->endDocument('json');
1062 function showXmlGroups($group)
1065 $this->initDocument('xml');
1066 $this->elementStart('groups', array('type' => 'array'));
1068 if (is_array($group)) {
1069 foreach ($group as $g) {
1070 $twitter_group = $this->twitterGroupArray($g);
1071 $this->showTwitterXmlGroup($twitter_group);
1074 while ($group->fetch()) {
1075 $twitter_group = $this->twitterGroupArray($group);
1076 $this->showTwitterXmlGroup($twitter_group);
1080 $this->elementEnd('groups');
1081 $this->endDocument('xml');
1084 function showTwitterXmlUsers($user)
1087 $this->initDocument('xml');
1088 $this->elementStart('users', array('type' => 'array',
1089 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1091 if (is_array($user)) {
1092 foreach ($user as $u) {
1093 $twitter_user = $this->twitterUserArray($u);
1094 $this->showTwitterXmlUser($twitter_user);
1097 while ($user->fetch()) {
1098 $twitter_user = $this->twitterUserArray($user);
1099 $this->showTwitterXmlUser($twitter_user);
1103 $this->elementEnd('users');
1104 $this->endDocument('xml');
1107 function showJsonUsers($user)
1110 $this->initDocument('json');
1114 if (is_array($user)) {
1115 foreach ($user as $u) {
1116 $twitter_user = $this->twitterUserArray($u);
1117 array_push($users, $twitter_user);
1120 while ($user->fetch()) {
1121 $twitter_user = $this->twitterUserArray($user);
1122 array_push($users, $twitter_user);
1126 $this->showJsonObjects($users);
1128 $this->endDocument('json');
1131 function showSingleJsonGroup($group)
1133 $this->initDocument('json');
1134 $twitter_group = $this->twitterGroupArray($group);
1135 $this->showJsonObjects($twitter_group);
1136 $this->endDocument('json');
1139 function showSingleXmlGroup($group)
1141 $this->initDocument('xml');
1142 $twitter_group = $this->twitterGroupArray($group);
1143 $this->showTwitterXmlGroup($twitter_group);
1144 $this->endDocument('xml');
1147 function dateTwitter($dt)
1149 $dateStr = date('d F Y H:i:s', strtotime($dt));
1150 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1151 $d->setTimezone(new DateTimeZone(common_timezone()));
1152 return $d->format('D M d H:i:s O Y');
1155 function initDocument($type='xml')
1159 header('Content-Type: application/xml; charset=utf-8');
1163 header('Content-Type: application/json; charset=utf-8');
1165 // Check for JSONP callback
1166 $callback = $this->arg('callback');
1168 print $callback . '(';
1172 header("Content-Type: application/rss+xml; charset=utf-8");
1173 $this->initTwitterRss();
1176 header('Content-Type: application/atom+xml; charset=utf-8');
1177 $this->initTwitterAtom();
1180 // TRANS: Client error on an API request with an unsupported data format.
1181 $this->clientError(_('Not a supported data format.'));
1188 function endDocument($type='xml')
1196 // Check for JSONP callback
1197 $callback = $this->arg('callback');
1203 $this->endTwitterRss();
1206 $this->endTwitterRss();
1209 // TRANS: Client error on an API request with an unsupported data format.
1210 $this->clientError(_('Not a supported data format.'));
1216 function clientError($msg, $code = 400, $format = 'xml')
1218 $action = $this->trimmed('action');
1220 common_debug("User error '$code' on '$action': $msg", __FILE__);
1222 if (!array_key_exists($code, ClientErrorAction::$status)) {
1226 $status_string = ClientErrorAction::$status[$code];
1228 header('HTTP/1.1 '.$code.' '.$status_string);
1230 if ($format == 'xml') {
1231 $this->initDocument('xml');
1232 $this->elementStart('hash');
1233 $this->element('error', null, $msg);
1234 $this->element('request', null, $_SERVER['REQUEST_URI']);
1235 $this->elementEnd('hash');
1236 $this->endDocument('xml');
1237 } elseif ($format == 'json'){
1238 $this->initDocument('json');
1239 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1240 print(json_encode($error_array));
1241 $this->endDocument('json');
1244 // If user didn't request a useful format, throw a regular client error
1245 throw new ClientException($msg, $code);
1249 function serverError($msg, $code = 500, $content_type = 'xml')
1251 $action = $this->trimmed('action');
1253 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1255 if (!array_key_exists($code, ServerErrorAction::$status)) {
1259 $status_string = ServerErrorAction::$status[$code];
1261 header('HTTP/1.1 '.$code.' '.$status_string);
1263 if ($content_type == 'xml') {
1264 $this->initDocument('xml');
1265 $this->elementStart('hash');
1266 $this->element('error', null, $msg);
1267 $this->element('request', null, $_SERVER['REQUEST_URI']);
1268 $this->elementEnd('hash');
1269 $this->endDocument('xml');
1271 $this->initDocument('json');
1272 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1273 print(json_encode($error_array));
1274 $this->endDocument('json');
1278 function initTwitterRss()
1281 $this->elementStart(
1285 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1286 'xmlns:georss' => 'http://www.georss.org/georss'
1289 $this->elementStart('channel');
1290 Event::handle('StartApiRss', array($this));
1293 function endTwitterRss()
1295 $this->elementEnd('channel');
1296 $this->elementEnd('rss');
1300 function initTwitterAtom()
1303 // FIXME: don't hardcode the language here!
1304 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1305 'xml:lang' => 'en-US',
1306 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1309 function endTwitterAtom()
1311 $this->elementEnd('feed');
1315 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1317 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1318 switch ($content_type) {
1320 $this->showTwitterXmlUser($profile_array);
1323 $this->showJsonObjects($profile_array);
1326 // TRANS: Client error on an API request with an unsupported data format.
1327 $this->clientError(_('Not a supported data format.'));
1333 function getTargetUser($id)
1337 // Twitter supports these other ways of passing the user ID
1338 if (is_numeric($this->arg('id'))) {
1339 return User::staticGet($this->arg('id'));
1340 } else if ($this->arg('id')) {
1341 $nickname = common_canonical_nickname($this->arg('id'));
1342 return User::staticGet('nickname', $nickname);
1343 } else if ($this->arg('user_id')) {
1344 // This is to ensure that a non-numeric user_id still
1345 // overrides screen_name even if it doesn't get used
1346 if (is_numeric($this->arg('user_id'))) {
1347 return User::staticGet('id', $this->arg('user_id'));
1349 } else if ($this->arg('screen_name')) {
1350 $nickname = common_canonical_nickname($this->arg('screen_name'));
1351 return User::staticGet('nickname', $nickname);
1353 // Fall back to trying the currently authenticated user
1354 return $this->auth_user;
1357 } else if (is_numeric($id)) {
1358 return User::staticGet($id);
1360 $nickname = common_canonical_nickname($id);
1361 return User::staticGet('nickname', $nickname);
1365 function getTargetGroup($id)
1368 if (is_numeric($this->arg('id'))) {
1369 return User_group::staticGet($this->arg('id'));
1370 } else if ($this->arg('id')) {
1371 $nickname = common_canonical_nickname($this->arg('id'));
1372 $local = Local_group::staticGet('nickname', $nickname);
1373 if (empty($local)) {
1376 return User_group::staticGet('id', $local->id);
1378 } else if ($this->arg('group_id')) {
1379 // This is to ensure that a non-numeric user_id still
1380 // overrides screen_name even if it doesn't get used
1381 if (is_numeric($this->arg('group_id'))) {
1382 return User_group::staticGet('id', $this->arg('group_id'));
1384 } else if ($this->arg('group_name')) {
1385 $nickname = common_canonical_nickname($this->arg('group_name'));
1386 $local = Local_group::staticGet('nickname', $nickname);
1387 if (empty($local)) {
1390 return User_group::staticGet('id', $local->group_id);
1394 } else if (is_numeric($id)) {
1395 return User_group::staticGet($id);
1397 $nickname = common_canonical_nickname($id);
1398 $local = Local_group::staticGet('nickname', $nickname);
1399 if (empty($local)) {
1402 return User_group::staticGet('id', $local->group_id);
1408 * Returns query argument or default value if not found. Certain
1409 * parameters used throughout the API are lightly scrubbed and
1410 * bounds checked. This overrides Action::arg().
1412 * @param string $key requested argument
1413 * @param string $def default value to return if $key is not provided
1417 function arg($key, $def=null)
1420 // XXX: Do even more input validation/scrubbing?
1422 if (array_key_exists($key, $this->args)) {
1425 $page = (int)$this->args['page'];
1426 return ($page < 1) ? 1 : $page;
1428 $count = (int)$this->args['count'];
1431 } elseif ($count > 200) {
1437 $since_id = (int)$this->args['since_id'];
1438 return ($since_id < 1) ? 0 : $since_id;
1440 $max_id = (int)$this->args['max_id'];
1441 return ($max_id < 1) ? 0 : $max_id;
1443 return parent::arg($key, $def);
1451 * Calculate the complete URI that called up this action. Used for
1452 * Atom rel="self" links. Warning: this is funky.
1454 * @return string URL a URL suitable for rel="self" Atom links
1456 function getSelfUri()
1458 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1460 $id = $this->arg('id');
1461 $aargs = array('format' => $this->format);
1466 $tag = $this->arg('tag');
1468 $aargs['tag'] = $tag;
1471 parse_str($_SERVER['QUERY_STRING'], $params);
1473 if (!empty($params)) {
1474 unset($params['p']);
1475 $pstring = http_build_query($params);
1478 $uri = common_local_url($action, $aargs);
1480 if (!empty($pstring)) {
1481 $uri .= '?' . $pstring;