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['statusnet:blocking'] = false;
277 $twitter_user['notifications'] = false;
279 if (isset($this->auth_user)) {
281 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
282 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
285 $sub = Subscription::pkeyGet(array('subscriber' =>
286 $this->auth_user->id,
287 'subscribed' => $profile->id));
290 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
295 $notice = $profile->getCurrentNotice();
298 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
302 // StatusNet-specific
304 $twitter_user['statusnet:profile_url'] = $profile->profileurl;
306 return $twitter_user;
309 function twitterStatusArray($notice, $include_user=true)
311 $base = $this->twitterSimpleStatusArray($notice, $include_user);
313 if (!empty($notice->repeat_of)) {
314 $original = Notice::staticGet('id', $notice->repeat_of);
315 if (!empty($original)) {
316 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
317 $base['retweeted_status'] = $original_array;
324 function twitterSimpleStatusArray($notice, $include_user=true)
326 $profile = $notice->getProfile();
328 $twitter_status = array();
329 $twitter_status['text'] = $notice->content;
330 $twitter_status['truncated'] = false; # Not possible on StatusNet
331 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
332 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
333 intval($notice->reply_to) : null;
337 $ns = $notice->getSource();
339 if (!empty($ns->name) && !empty($ns->url)) {
340 $source = '<a href="'
341 . htmlspecialchars($ns->url)
342 . '" rel="nofollow">'
343 . htmlspecialchars($ns->name)
350 $twitter_status['source'] = $source;
351 $twitter_status['id'] = intval($notice->id);
353 $replier_profile = null;
355 if ($notice->reply_to) {
356 $reply = Notice::staticGet(intval($notice->reply_to));
358 $replier_profile = $reply->getProfile();
362 $twitter_status['in_reply_to_user_id'] =
363 ($replier_profile) ? intval($replier_profile->id) : null;
364 $twitter_status['in_reply_to_screen_name'] =
365 ($replier_profile) ? $replier_profile->nickname : null;
367 if (isset($notice->lat) && isset($notice->lon)) {
368 // This is the format that GeoJSON expects stuff to be in
369 $twitter_status['geo'] = array('type' => 'Point',
370 'coordinates' => array((float) $notice->lat,
371 (float) $notice->lon));
373 $twitter_status['geo'] = null;
376 if (isset($this->auth_user)) {
377 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
379 $twitter_status['favorited'] = false;
383 $attachments = $notice->attachments();
385 if (!empty($attachments)) {
387 $twitter_status['attachments'] = array();
389 foreach ($attachments as $attachment) {
390 $enclosure_o=$attachment->getEnclosure();
392 $enclosure = array();
393 $enclosure['url'] = $enclosure_o->url;
394 $enclosure['mimetype'] = $enclosure_o->mimetype;
395 $enclosure['size'] = $enclosure_o->size;
396 $twitter_status['attachments'][] = $enclosure;
401 if ($include_user && $profile) {
402 # Don't get notice (recursive!)
403 $twitter_user = $this->twitterUserArray($profile, false);
404 $twitter_status['user'] = $twitter_user;
407 // StatusNet-specific
409 $twitter_status['statusnet:html'] = $notice->rendered;
411 return $twitter_status;
414 function twitterGroupArray($group)
416 $twitter_group = array();
418 $twitter_group['id'] = $group->id;
419 $twitter_group['url'] = $group->permalink();
420 $twitter_group['nickname'] = $group->nickname;
421 $twitter_group['fullname'] = $group->fullname;
423 if (isset($this->auth_user)) {
424 $twitter_group['member'] = $this->auth_user->isMember($group);
425 $twitter_group['blocked'] = Group_block::isBlocked(
427 $this->auth_user->getProfile()
431 $twitter_group['member_count'] = $group->getMemberCount();
432 $twitter_group['original_logo'] = $group->original_logo;
433 $twitter_group['homepage_logo'] = $group->homepage_logo;
434 $twitter_group['stream_logo'] = $group->stream_logo;
435 $twitter_group['mini_logo'] = $group->mini_logo;
436 $twitter_group['homepage'] = $group->homepage;
437 $twitter_group['description'] = $group->description;
438 $twitter_group['location'] = $group->location;
439 $twitter_group['created'] = $this->dateTwitter($group->created);
440 $twitter_group['modified'] = $this->dateTwitter($group->modified);
442 return $twitter_group;
445 function twitterRssGroupArray($group)
448 $entry['content']=$group->description;
449 $entry['title']=$group->nickname;
450 $entry['link']=$group->permalink();
451 $entry['published']=common_date_iso8601($group->created);
452 $entry['updated']==common_date_iso8601($group->modified);
453 $taguribase = common_config('integration', 'groupuri');
454 $entry['id'] = "group:$groupuribase:$entry[link]";
456 $entry['description'] = $entry['content'];
457 $entry['pubDate'] = common_date_rfc2822($group->created);
458 $entry['guid'] = $entry['link'];
463 function twitterRssEntryArray($notice)
465 $profile = $notice->getProfile();
468 // We trim() to avoid extraneous whitespace in the output
470 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
471 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
472 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
473 $entry['published'] = common_date_iso8601($notice->created);
475 $taguribase = TagURI::base();
476 $entry['id'] = "tag:$taguribase:$entry[link]";
478 $entry['updated'] = $entry['published'];
479 $entry['author'] = $profile->getBestName();
482 $attachments = $notice->attachments();
483 $enclosures = array();
485 foreach ($attachments as $attachment) {
486 $enclosure_o=$attachment->getEnclosure();
488 $enclosure = array();
489 $enclosure['url'] = $enclosure_o->url;
490 $enclosure['mimetype'] = $enclosure_o->mimetype;
491 $enclosure['size'] = $enclosure_o->size;
492 $enclosures[] = $enclosure;
496 if (!empty($enclosures)) {
497 $entry['enclosures'] = $enclosures;
501 $tag = new Notice_tag();
502 $tag->notice_id = $notice->id;
504 $entry['tags']=array();
505 while ($tag->fetch()) {
506 $entry['tags'][]=$tag->tag;
512 $entry['description'] = $entry['content'];
513 $entry['pubDate'] = common_date_rfc2822($notice->created);
514 $entry['guid'] = $entry['link'];
516 if (isset($notice->lat) && isset($notice->lon)) {
517 // This is the format that GeoJSON expects stuff to be in.
518 // showGeoRSS() below uses it for XML output, so we reuse it
519 $entry['geo'] = array('type' => 'Point',
520 'coordinates' => array((float) $notice->lat,
521 (float) $notice->lon));
523 $entry['geo'] = null;
529 function twitterRelationshipArray($source, $target)
531 $relationship = array();
533 $relationship['source'] =
534 $this->relationshipDetailsArray($source, $target);
535 $relationship['target'] =
536 $this->relationshipDetailsArray($target, $source);
538 return array('relationship' => $relationship);
541 function relationshipDetailsArray($source, $target)
545 $details['screen_name'] = $source->nickname;
546 $details['followed_by'] = $target->isSubscribed($source);
547 $details['following'] = $source->isSubscribed($target);
549 $notifications = false;
551 if ($source->isSubscribed($target)) {
553 $sub = Subscription::pkeyGet(array('subscriber' =>
554 $source->id, 'subscribed' => $target->id));
557 $notifications = ($sub->jabber || $sub->sms);
561 $details['notifications_enabled'] = $notifications;
562 $details['blocking'] = $source->hasBlocked($target);
563 $details['id'] = $source->id;
568 function showTwitterXmlRelationship($relationship)
570 $this->elementStart('relationship');
572 foreach($relationship as $element => $value) {
573 if ($element == 'source' || $element == 'target') {
574 $this->elementStart($element);
575 $this->showXmlRelationshipDetails($value);
576 $this->elementEnd($element);
580 $this->elementEnd('relationship');
583 function showXmlRelationshipDetails($details)
585 foreach($details as $element => $value) {
586 $this->element($element, null, $value);
590 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
594 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
596 $this->elementStart($tag, $attrs);
597 foreach($twitter_status as $element => $value) {
600 $this->showTwitterXmlUser($twitter_status['user']);
603 $this->element($element, null, common_xml_safe_str($value));
606 $this->showXmlAttachments($twitter_status['attachments']);
609 $this->showGeoXML($value);
611 case 'retweeted_status':
612 $this->showTwitterXmlStatus($value, 'retweeted_status');
615 $this->element($element, null, $value);
618 $this->elementEnd($tag);
621 function showTwitterXmlGroup($twitter_group)
623 $this->elementStart('group');
624 foreach($twitter_group as $element => $value) {
625 $this->element($element, null, $value);
627 $this->elementEnd('group');
630 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
634 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
636 $this->elementStart($role, $attrs);
637 foreach($twitter_user as $element => $value) {
638 if ($element == 'status') {
639 $this->showTwitterXmlStatus($twitter_user['status']);
641 $this->element($element, null, $value);
644 $this->elementEnd($role);
647 function showXmlAttachments($attachments) {
648 if (!empty($attachments)) {
649 $this->elementStart('attachments', array('type' => 'array'));
650 foreach ($attachments as $attachment) {
652 $attrs['url'] = $attachment['url'];
653 $attrs['mimetype'] = $attachment['mimetype'];
654 $attrs['size'] = $attachment['size'];
655 $this->element('enclosure', $attrs, '');
657 $this->elementEnd('attachments');
661 function showGeoXML($geo)
665 $this->element('geo');
667 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
668 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
669 $this->elementEnd('geo');
673 function showGeoRSS($geo)
679 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
684 function showTwitterRssItem($entry)
686 $this->elementStart('item');
687 $this->element('title', null, $entry['title']);
688 $this->element('description', null, $entry['description']);
689 $this->element('pubDate', null, $entry['pubDate']);
690 $this->element('guid', null, $entry['guid']);
691 $this->element('link', null, $entry['link']);
693 # RSS only supports 1 enclosure per item
694 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
695 $enclosure = $entry['enclosures'][0];
696 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
699 if(array_key_exists('tags', $entry)){
700 foreach($entry['tags'] as $tag){
701 $this->element('category', null,$tag);
705 $this->showGeoRSS($entry['geo']);
706 $this->elementEnd('item');
709 function showJsonObjects($objects)
711 print(json_encode($objects));
714 function showSingleXmlStatus($notice)
716 $this->initDocument('xml');
717 $twitter_status = $this->twitterStatusArray($notice);
718 $this->showTwitterXmlStatus($twitter_status, 'status', true);
719 $this->endDocument('xml');
722 function show_single_json_status($notice)
724 $this->initDocument('json');
725 $status = $this->twitterStatusArray($notice);
726 $this->showJsonObjects($status);
727 $this->endDocument('json');
730 function showXmlTimeline($notice)
733 $this->initDocument('xml');
734 $this->elementStart('statuses', array('type' => 'array',
735 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
737 if (is_array($notice)) {
738 foreach ($notice as $n) {
739 $twitter_status = $this->twitterStatusArray($n);
740 $this->showTwitterXmlStatus($twitter_status);
743 while ($notice->fetch()) {
744 $twitter_status = $this->twitterStatusArray($notice);
745 $this->showTwitterXmlStatus($twitter_status);
749 $this->elementEnd('statuses');
750 $this->endDocument('xml');
753 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
756 $this->initDocument('rss');
758 $this->element('title', null, $title);
759 $this->element('link', null, $link);
761 if (!is_null($self)) {
765 'type' => 'application/rss+xml',
772 if (!is_null($suplink)) {
773 // For FriendFeed's SUP protocol
774 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
775 'rel' => 'http://api.friendfeed.com/2008/03#sup',
777 'type' => 'application/json'));
780 if (!is_null($logo)) {
781 $this->elementStart('image');
782 $this->element('link', null, $link);
783 $this->element('title', null, $title);
784 $this->element('url', null, $logo);
785 $this->elementEnd('image');
788 $this->element('description', null, $subtitle);
789 $this->element('language', null, 'en-us');
790 $this->element('ttl', null, '40');
792 if (is_array($notice)) {
793 foreach ($notice as $n) {
794 $entry = $this->twitterRssEntryArray($n);
795 $this->showTwitterRssItem($entry);
798 while ($notice->fetch()) {
799 $entry = $this->twitterRssEntryArray($notice);
800 $this->showTwitterRssItem($entry);
804 $this->endTwitterRss();
807 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
810 $this->initDocument('atom');
812 $this->element('title', null, $title);
813 $this->element('id', null, $id);
814 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
816 if (!is_null($logo)) {
817 $this->element('logo',null,$logo);
820 if (!is_null($suplink)) {
821 # For FriendFeed's SUP protocol
822 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
824 'type' => 'application/json'));
827 if (!is_null($selfuri)) {
828 $this->element('link', array('href' => $selfuri,
829 'rel' => 'self', 'type' => 'application/atom+xml'), null);
832 $this->element('updated', null, common_date_iso8601('now'));
833 $this->element('subtitle', null, $subtitle);
835 if (is_array($notice)) {
836 foreach ($notice as $n) {
837 $this->raw($n->asAtomEntry());
840 while ($notice->fetch()) {
841 $this->raw($notice->asAtomEntry());
845 $this->endDocument('atom');
849 function showRssGroups($group, $title, $link, $subtitle)
852 $this->initDocument('rss');
854 $this->element('title', null, $title);
855 $this->element('link', null, $link);
856 $this->element('description', null, $subtitle);
857 $this->element('language', null, 'en-us');
858 $this->element('ttl', null, '40');
860 if (is_array($group)) {
861 foreach ($group as $g) {
862 $twitter_group = $this->twitterRssGroupArray($g);
863 $this->showTwitterRssItem($twitter_group);
866 while ($group->fetch()) {
867 $twitter_group = $this->twitterRssGroupArray($group);
868 $this->showTwitterRssItem($twitter_group);
872 $this->endTwitterRss();
875 function showTwitterAtomEntry($entry)
877 $this->elementStart('entry');
878 $this->element('title', null, common_xml_safe_str($entry['title']));
881 array('type' => 'html'),
882 common_xml_safe_str($entry['content'])
884 $this->element('id', null, $entry['id']);
885 $this->element('published', null, $entry['published']);
886 $this->element('updated', null, $entry['updated']);
887 $this->element('link', array('type' => 'text/html',
888 'href' => $entry['link'],
889 'rel' => 'alternate'));
890 $this->element('link', array('type' => $entry['avatar-type'],
891 'href' => $entry['avatar'],
893 $this->elementStart('author');
895 $this->element('name', null, $entry['author-name']);
896 $this->element('uri', null, $entry['author-uri']);
898 $this->elementEnd('author');
899 $this->elementEnd('entry');
902 function showXmlDirectMessage($dm, $namespaces=false)
906 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
908 $this->elementStart('direct_message', $attrs);
909 foreach($dm as $element => $value) {
913 $this->showTwitterXmlUser($value, $element);
916 $this->element($element, null, common_xml_safe_str($value));
919 $this->element($element, null, $value);
923 $this->elementEnd('direct_message');
926 function directMessageArray($message)
930 $from_profile = $message->getFrom();
931 $to_profile = $message->getTo();
933 $dmsg['id'] = $message->id;
934 $dmsg['sender_id'] = $message->from_profile;
935 $dmsg['text'] = trim($message->content);
936 $dmsg['recipient_id'] = $message->to_profile;
937 $dmsg['created_at'] = $this->dateTwitter($message->created);
938 $dmsg['sender_screen_name'] = $from_profile->nickname;
939 $dmsg['recipient_screen_name'] = $to_profile->nickname;
940 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
941 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
946 function rssDirectMessageArray($message)
950 $from = $message->getFrom();
952 $entry['title'] = sprintf('Message from %1$s to %2$s',
953 $from->nickname, $message->getTo()->nickname);
955 $entry['content'] = common_xml_safe_str($message->rendered);
956 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
957 $entry['published'] = common_date_iso8601($message->created);
959 $taguribase = TagURI::base();
961 $entry['id'] = "tag:$taguribase:$entry[link]";
962 $entry['updated'] = $entry['published'];
964 $entry['author-name'] = $from->getBestName();
965 $entry['author-uri'] = $from->homepage;
967 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
969 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
970 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
974 $entry['description'] = $entry['content'];
975 $entry['pubDate'] = common_date_rfc2822($message->created);
976 $entry['guid'] = $entry['link'];
981 function showSingleXmlDirectMessage($message)
983 $this->initDocument('xml');
984 $dmsg = $this->directMessageArray($message);
985 $this->showXmlDirectMessage($dmsg, true);
986 $this->endDocument('xml');
989 function showSingleJsonDirectMessage($message)
991 $this->initDocument('json');
992 $dmsg = $this->directMessageArray($message);
993 $this->showJsonObjects($dmsg);
994 $this->endDocument('json');
997 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1000 $this->initDocument('atom');
1002 $this->element('title', null, common_xml_safe_str($title));
1003 $this->element('id', null, $id);
1004 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1006 if (!is_null($selfuri)) {
1007 $this->element('link', array('href' => $selfuri,
1008 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1011 $this->element('updated', null, common_date_iso8601('now'));
1012 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1014 if (is_array($group)) {
1015 foreach ($group as $g) {
1016 $this->raw($g->asAtomEntry());
1019 while ($group->fetch()) {
1020 $this->raw($group->asAtomEntry());
1024 $this->endDocument('atom');
1028 function showJsonTimeline($notice)
1031 $this->initDocument('json');
1033 $statuses = array();
1035 if (is_array($notice)) {
1036 foreach ($notice as $n) {
1037 $twitter_status = $this->twitterStatusArray($n);
1038 array_push($statuses, $twitter_status);
1041 while ($notice->fetch()) {
1042 $twitter_status = $this->twitterStatusArray($notice);
1043 array_push($statuses, $twitter_status);
1047 $this->showJsonObjects($statuses);
1049 $this->endDocument('json');
1052 function showJsonGroups($group)
1055 $this->initDocument('json');
1059 if (is_array($group)) {
1060 foreach ($group as $g) {
1061 $twitter_group = $this->twitterGroupArray($g);
1062 array_push($groups, $twitter_group);
1065 while ($group->fetch()) {
1066 $twitter_group = $this->twitterGroupArray($group);
1067 array_push($groups, $twitter_group);
1071 $this->showJsonObjects($groups);
1073 $this->endDocument('json');
1076 function showXmlGroups($group)
1079 $this->initDocument('xml');
1080 $this->elementStart('groups', array('type' => 'array'));
1082 if (is_array($group)) {
1083 foreach ($group as $g) {
1084 $twitter_group = $this->twitterGroupArray($g);
1085 $this->showTwitterXmlGroup($twitter_group);
1088 while ($group->fetch()) {
1089 $twitter_group = $this->twitterGroupArray($group);
1090 $this->showTwitterXmlGroup($twitter_group);
1094 $this->elementEnd('groups');
1095 $this->endDocument('xml');
1098 function showTwitterXmlUsers($user)
1101 $this->initDocument('xml');
1102 $this->elementStart('users', array('type' => 'array',
1103 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1105 if (is_array($user)) {
1106 foreach ($user as $u) {
1107 $twitter_user = $this->twitterUserArray($u);
1108 $this->showTwitterXmlUser($twitter_user);
1111 while ($user->fetch()) {
1112 $twitter_user = $this->twitterUserArray($user);
1113 $this->showTwitterXmlUser($twitter_user);
1117 $this->elementEnd('users');
1118 $this->endDocument('xml');
1121 function showJsonUsers($user)
1124 $this->initDocument('json');
1128 if (is_array($user)) {
1129 foreach ($user as $u) {
1130 $twitter_user = $this->twitterUserArray($u);
1131 array_push($users, $twitter_user);
1134 while ($user->fetch()) {
1135 $twitter_user = $this->twitterUserArray($user);
1136 array_push($users, $twitter_user);
1140 $this->showJsonObjects($users);
1142 $this->endDocument('json');
1145 function showSingleJsonGroup($group)
1147 $this->initDocument('json');
1148 $twitter_group = $this->twitterGroupArray($group);
1149 $this->showJsonObjects($twitter_group);
1150 $this->endDocument('json');
1153 function showSingleXmlGroup($group)
1155 $this->initDocument('xml');
1156 $twitter_group = $this->twitterGroupArray($group);
1157 $this->showTwitterXmlGroup($twitter_group);
1158 $this->endDocument('xml');
1161 function dateTwitter($dt)
1163 $dateStr = date('d F Y H:i:s', strtotime($dt));
1164 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1165 $d->setTimezone(new DateTimeZone(common_timezone()));
1166 return $d->format('D M d H:i:s O Y');
1169 function initDocument($type='xml')
1173 header('Content-Type: application/xml; charset=utf-8');
1177 header('Content-Type: application/json; charset=utf-8');
1179 // Check for JSONP callback
1180 $callback = $this->arg('callback');
1182 print $callback . '(';
1186 header("Content-Type: application/rss+xml; charset=utf-8");
1187 $this->initTwitterRss();
1190 header('Content-Type: application/atom+xml; charset=utf-8');
1191 $this->initTwitterAtom();
1194 // TRANS: Client error on an API request with an unsupported data format.
1195 $this->clientError(_('Not a supported data format.'));
1202 function endDocument($type='xml')
1210 // Check for JSONP callback
1211 $callback = $this->arg('callback');
1217 $this->endTwitterRss();
1220 $this->endTwitterRss();
1223 // TRANS: Client error on an API request with an unsupported data format.
1224 $this->clientError(_('Not a supported data format.'));
1230 function clientError($msg, $code = 400, $format = 'xml')
1232 $action = $this->trimmed('action');
1234 common_debug("User error '$code' on '$action': $msg", __FILE__);
1236 if (!array_key_exists($code, ClientErrorAction::$status)) {
1240 $status_string = ClientErrorAction::$status[$code];
1242 header('HTTP/1.1 '.$code.' '.$status_string);
1244 if ($format == 'xml') {
1245 $this->initDocument('xml');
1246 $this->elementStart('hash');
1247 $this->element('error', null, $msg);
1248 $this->element('request', null, $_SERVER['REQUEST_URI']);
1249 $this->elementEnd('hash');
1250 $this->endDocument('xml');
1251 } elseif ($format == 'json'){
1252 $this->initDocument('json');
1253 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1254 print(json_encode($error_array));
1255 $this->endDocument('json');
1258 // If user didn't request a useful format, throw a regular client error
1259 throw new ClientException($msg, $code);
1263 function serverError($msg, $code = 500, $content_type = 'xml')
1265 $action = $this->trimmed('action');
1267 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1269 if (!array_key_exists($code, ServerErrorAction::$status)) {
1273 $status_string = ServerErrorAction::$status[$code];
1275 header('HTTP/1.1 '.$code.' '.$status_string);
1277 if ($content_type == 'xml') {
1278 $this->initDocument('xml');
1279 $this->elementStart('hash');
1280 $this->element('error', null, $msg);
1281 $this->element('request', null, $_SERVER['REQUEST_URI']);
1282 $this->elementEnd('hash');
1283 $this->endDocument('xml');
1285 $this->initDocument('json');
1286 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1287 print(json_encode($error_array));
1288 $this->endDocument('json');
1292 function initTwitterRss()
1295 $this->elementStart(
1299 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1300 'xmlns:georss' => 'http://www.georss.org/georss'
1303 $this->elementStart('channel');
1304 Event::handle('StartApiRss', array($this));
1307 function endTwitterRss()
1309 $this->elementEnd('channel');
1310 $this->elementEnd('rss');
1314 function initTwitterAtom()
1317 // FIXME: don't hardcode the language here!
1318 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1319 'xml:lang' => 'en-US',
1320 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1323 function endTwitterAtom()
1325 $this->elementEnd('feed');
1329 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1331 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1332 switch ($content_type) {
1334 $this->showTwitterXmlUser($profile_array);
1337 $this->showJsonObjects($profile_array);
1340 // TRANS: Client error on an API request with an unsupported data format.
1341 $this->clientError(_('Not a supported data format.'));
1347 function getTargetUser($id)
1351 // Twitter supports these other ways of passing the user ID
1352 if (is_numeric($this->arg('id'))) {
1353 return User::staticGet($this->arg('id'));
1354 } else if ($this->arg('id')) {
1355 $nickname = common_canonical_nickname($this->arg('id'));
1356 return User::staticGet('nickname', $nickname);
1357 } else if ($this->arg('user_id')) {
1358 // This is to ensure that a non-numeric user_id still
1359 // overrides screen_name even if it doesn't get used
1360 if (is_numeric($this->arg('user_id'))) {
1361 return User::staticGet('id', $this->arg('user_id'));
1363 } else if ($this->arg('screen_name')) {
1364 $nickname = common_canonical_nickname($this->arg('screen_name'));
1365 return User::staticGet('nickname', $nickname);
1367 // Fall back to trying the currently authenticated user
1368 return $this->auth_user;
1371 } else if (is_numeric($id)) {
1372 return User::staticGet($id);
1374 $nickname = common_canonical_nickname($id);
1375 return User::staticGet('nickname', $nickname);
1379 function getTargetGroup($id)
1382 if (is_numeric($this->arg('id'))) {
1383 return User_group::staticGet($this->arg('id'));
1384 } else if ($this->arg('id')) {
1385 $nickname = common_canonical_nickname($this->arg('id'));
1386 $local = Local_group::staticGet('nickname', $nickname);
1387 if (empty($local)) {
1390 return User_group::staticGet('id', $local->id);
1392 } else if ($this->arg('group_id')) {
1393 // This is to ensure that a non-numeric user_id still
1394 // overrides screen_name even if it doesn't get used
1395 if (is_numeric($this->arg('group_id'))) {
1396 return User_group::staticGet('id', $this->arg('group_id'));
1398 } else if ($this->arg('group_name')) {
1399 $nickname = common_canonical_nickname($this->arg('group_name'));
1400 $local = Local_group::staticGet('nickname', $nickname);
1401 if (empty($local)) {
1404 return User_group::staticGet('id', $local->group_id);
1408 } else if (is_numeric($id)) {
1409 return User_group::staticGet($id);
1411 $nickname = common_canonical_nickname($id);
1412 $local = Local_group::staticGet('nickname', $nickname);
1413 if (empty($local)) {
1416 return User_group::staticGet('id', $local->group_id);
1422 * Returns query argument or default value if not found. Certain
1423 * parameters used throughout the API are lightly scrubbed and
1424 * bounds checked. This overrides Action::arg().
1426 * @param string $key requested argument
1427 * @param string $def default value to return if $key is not provided
1431 function arg($key, $def=null)
1434 // XXX: Do even more input validation/scrubbing?
1436 if (array_key_exists($key, $this->args)) {
1439 $page = (int)$this->args['page'];
1440 return ($page < 1) ? 1 : $page;
1442 $count = (int)$this->args['count'];
1445 } elseif ($count > 200) {
1451 $since_id = (int)$this->args['since_id'];
1452 return ($since_id < 1) ? 0 : $since_id;
1454 $max_id = (int)$this->args['max_id'];
1455 return ($max_id < 1) ? 0 : $max_id;
1457 return parent::arg($key, $def);
1465 * Calculate the complete URI that called up this action. Used for
1466 * Atom rel="self" links. Warning: this is funky.
1468 * @return string URL a URL suitable for rel="self" Atom links
1470 function getSelfUri()
1472 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1474 $id = $this->arg('id');
1475 $aargs = array('format' => $this->format);
1480 $tag = $this->arg('tag');
1482 $aargs['tag'] = $tag;
1485 parse_str($_SERVER['QUERY_STRING'], $params);
1487 if (!empty($params)) {
1488 unset($params['p']);
1489 $pstring = http_build_query($params);
1492 $uri = common_local_url($action, $aargs);
1494 if (!empty($pstring)) {
1495 $uri .= '?' . $pstring;