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;
129 var $callback = null;
131 var $access = self::READ_ONLY; // read (default) or read-write
133 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
138 * @param array $args Web and URL arguments
140 * @return boolean false if user doesn't exist
143 function prepare($args)
145 StatusNet::setApi(true); // reduce exception reports to aid in debugging
146 parent::prepare($args);
148 $this->format = $this->arg('format');
149 $this->callback = $this->arg('callback');
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();
469 // We trim() to avoid extraneous whitespace in the output
471 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
472 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
473 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
474 $entry['published'] = common_date_iso8601($notice->created);
476 $taguribase = TagURI::base();
477 $entry['id'] = "tag:$taguribase:$entry[link]";
479 $entry['updated'] = $entry['published'];
480 $entry['author'] = $profile->getBestName();
483 $attachments = $notice->attachments();
484 $enclosures = array();
486 foreach ($attachments as $attachment) {
487 $enclosure_o=$attachment->getEnclosure();
489 $enclosure = array();
490 $enclosure['url'] = $enclosure_o->url;
491 $enclosure['mimetype'] = $enclosure_o->mimetype;
492 $enclosure['size'] = $enclosure_o->size;
493 $enclosures[] = $enclosure;
497 if (!empty($enclosures)) {
498 $entry['enclosures'] = $enclosures;
502 $tag = new Notice_tag();
503 $tag->notice_id = $notice->id;
505 $entry['tags']=array();
506 while ($tag->fetch()) {
507 $entry['tags'][]=$tag->tag;
513 $entry['description'] = $entry['content'];
514 $entry['pubDate'] = common_date_rfc2822($notice->created);
515 $entry['guid'] = $entry['link'];
517 if (isset($notice->lat) && isset($notice->lon)) {
518 // This is the format that GeoJSON expects stuff to be in.
519 // showGeoRSS() below uses it for XML output, so we reuse it
520 $entry['geo'] = array('type' => 'Point',
521 'coordinates' => array((float) $notice->lat,
522 (float) $notice->lon));
524 $entry['geo'] = null;
530 function twitterRelationshipArray($source, $target)
532 $relationship = array();
534 $relationship['source'] =
535 $this->relationshipDetailsArray($source, $target);
536 $relationship['target'] =
537 $this->relationshipDetailsArray($target, $source);
539 return array('relationship' => $relationship);
542 function relationshipDetailsArray($source, $target)
546 $details['screen_name'] = $source->nickname;
547 $details['followed_by'] = $target->isSubscribed($source);
548 $details['following'] = $source->isSubscribed($target);
550 $notifications = false;
552 if ($source->isSubscribed($target)) {
554 $sub = Subscription::pkeyGet(array('subscriber' =>
555 $source->id, 'subscribed' => $target->id));
558 $notifications = ($sub->jabber || $sub->sms);
562 $details['notifications_enabled'] = $notifications;
563 $details['blocking'] = $source->hasBlocked($target);
564 $details['id'] = $source->id;
569 function showTwitterXmlRelationship($relationship)
571 $this->elementStart('relationship');
573 foreach($relationship as $element => $value) {
574 if ($element == 'source' || $element == 'target') {
575 $this->elementStart($element);
576 $this->showXmlRelationshipDetails($value);
577 $this->elementEnd($element);
581 $this->elementEnd('relationship');
584 function showXmlRelationshipDetails($details)
586 foreach($details as $element => $value) {
587 $this->element($element, null, $value);
591 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
595 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
597 $this->elementStart($tag, $attrs);
598 foreach($twitter_status as $element => $value) {
601 $this->showTwitterXmlUser($twitter_status['user']);
604 $this->element($element, null, common_xml_safe_str($value));
607 $this->showXmlAttachments($twitter_status['attachments']);
610 $this->showGeoXML($value);
612 case 'retweeted_status':
613 $this->showTwitterXmlStatus($value, 'retweeted_status');
616 $this->element($element, null, $value);
619 $this->elementEnd($tag);
622 function showTwitterXmlGroup($twitter_group)
624 $this->elementStart('group');
625 foreach($twitter_group as $element => $value) {
626 $this->element($element, null, $value);
628 $this->elementEnd('group');
631 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
635 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
637 $this->elementStart($role, $attrs);
638 foreach($twitter_user as $element => $value) {
639 if ($element == 'status') {
640 $this->showTwitterXmlStatus($twitter_user['status']);
642 $this->element($element, null, $value);
645 $this->elementEnd($role);
648 function showXmlAttachments($attachments) {
649 if (!empty($attachments)) {
650 $this->elementStart('attachments', array('type' => 'array'));
651 foreach ($attachments as $attachment) {
653 $attrs['url'] = $attachment['url'];
654 $attrs['mimetype'] = $attachment['mimetype'];
655 $attrs['size'] = $attachment['size'];
656 $this->element('enclosure', $attrs, '');
658 $this->elementEnd('attachments');
662 function showGeoXML($geo)
666 $this->element('geo');
668 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
669 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
670 $this->elementEnd('geo');
674 function showGeoRSS($geo)
680 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
685 function showTwitterRssItem($entry)
687 $this->elementStart('item');
688 $this->element('title', null, $entry['title']);
689 $this->element('description', null, $entry['description']);
690 $this->element('pubDate', null, $entry['pubDate']);
691 $this->element('guid', null, $entry['guid']);
692 $this->element('link', null, $entry['link']);
694 # RSS only supports 1 enclosure per item
695 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
696 $enclosure = $entry['enclosures'][0];
697 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
700 if(array_key_exists('tags', $entry)){
701 foreach($entry['tags'] as $tag){
702 $this->element('category', null,$tag);
706 $this->showGeoRSS($entry['geo']);
707 $this->elementEnd('item');
710 function showJsonObjects($objects)
712 print(json_encode($objects));
715 function showSingleXmlStatus($notice)
717 $this->initDocument('xml');
718 $twitter_status = $this->twitterStatusArray($notice);
719 $this->showTwitterXmlStatus($twitter_status, 'status', true);
720 $this->endDocument('xml');
723 function show_single_json_status($notice)
725 $this->initDocument('json');
726 $status = $this->twitterStatusArray($notice);
727 $this->showJsonObjects($status);
728 $this->endDocument('json');
731 function showXmlTimeline($notice)
734 $this->initDocument('xml');
735 $this->elementStart('statuses', array('type' => 'array',
736 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
738 if (is_array($notice)) {
739 $notice = new ArrayWrapper($notice);
742 while ($notice->fetch()) {
744 $twitter_status = $this->twitterStatusArray($notice);
745 $this->showTwitterXmlStatus($twitter_status);
746 } catch (Exception $e) {
747 common_log(LOG_ERR, $e->getMessage());
752 $this->elementEnd('statuses');
753 $this->endDocument('xml');
756 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
759 $this->initDocument('rss');
761 $this->element('title', null, $title);
762 $this->element('link', null, $link);
764 if (!is_null($self)) {
768 'type' => 'application/rss+xml',
775 if (!is_null($suplink)) {
776 // For FriendFeed's SUP protocol
777 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
778 'rel' => 'http://api.friendfeed.com/2008/03#sup',
780 'type' => 'application/json'));
783 if (!is_null($logo)) {
784 $this->elementStart('image');
785 $this->element('link', null, $link);
786 $this->element('title', null, $title);
787 $this->element('url', null, $logo);
788 $this->elementEnd('image');
791 $this->element('description', null, $subtitle);
792 $this->element('language', null, 'en-us');
793 $this->element('ttl', null, '40');
795 if (is_array($notice)) {
796 $notice = new ArrayWrapper($notice);
799 while ($notice->fetch()) {
801 $entry = $this->twitterRssEntryArray($notice);
802 $this->showTwitterRssItem($entry);
803 } catch (Exception $e) {
804 common_log(LOG_ERR, $e->getMessage());
805 // continue on exceptions
809 $this->endTwitterRss();
812 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
815 $this->initDocument('atom');
817 $this->element('title', null, $title);
818 $this->element('id', null, $id);
819 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
821 if (!is_null($logo)) {
822 $this->element('logo',null,$logo);
825 if (!is_null($suplink)) {
826 # For FriendFeed's SUP protocol
827 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
829 'type' => 'application/json'));
832 if (!is_null($selfuri)) {
833 $this->element('link', array('href' => $selfuri,
834 'rel' => 'self', 'type' => 'application/atom+xml'), null);
837 $this->element('updated', null, common_date_iso8601('now'));
838 $this->element('subtitle', null, $subtitle);
840 if (is_array($notice)) {
841 $notice = new ArrayWrapper($notice);
844 while ($notice->fetch()) {
846 $this->raw($notice->asAtomEntry());
847 } catch (Exception $e) {
848 common_log(LOG_ERR, $e->getMessage());
853 $this->endDocument('atom');
857 function showRssGroups($group, $title, $link, $subtitle)
860 $this->initDocument('rss');
862 $this->element('title', null, $title);
863 $this->element('link', null, $link);
864 $this->element('description', null, $subtitle);
865 $this->element('language', null, 'en-us');
866 $this->element('ttl', null, '40');
868 if (is_array($group)) {
869 foreach ($group as $g) {
870 $twitter_group = $this->twitterRssGroupArray($g);
871 $this->showTwitterRssItem($twitter_group);
874 while ($group->fetch()) {
875 $twitter_group = $this->twitterRssGroupArray($group);
876 $this->showTwitterRssItem($twitter_group);
880 $this->endTwitterRss();
883 function showTwitterAtomEntry($entry)
885 $this->elementStart('entry');
886 $this->element('title', null, common_xml_safe_str($entry['title']));
889 array('type' => 'html'),
890 common_xml_safe_str($entry['content'])
892 $this->element('id', null, $entry['id']);
893 $this->element('published', null, $entry['published']);
894 $this->element('updated', null, $entry['updated']);
895 $this->element('link', array('type' => 'text/html',
896 'href' => $entry['link'],
897 'rel' => 'alternate'));
898 $this->element('link', array('type' => $entry['avatar-type'],
899 'href' => $entry['avatar'],
901 $this->elementStart('author');
903 $this->element('name', null, $entry['author-name']);
904 $this->element('uri', null, $entry['author-uri']);
906 $this->elementEnd('author');
907 $this->elementEnd('entry');
910 function showXmlDirectMessage($dm, $namespaces=false)
914 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
916 $this->elementStart('direct_message', $attrs);
917 foreach($dm as $element => $value) {
921 $this->showTwitterXmlUser($value, $element);
924 $this->element($element, null, common_xml_safe_str($value));
927 $this->element($element, null, $value);
931 $this->elementEnd('direct_message');
934 function directMessageArray($message)
938 $from_profile = $message->getFrom();
939 $to_profile = $message->getTo();
941 $dmsg['id'] = $message->id;
942 $dmsg['sender_id'] = $message->from_profile;
943 $dmsg['text'] = trim($message->content);
944 $dmsg['recipient_id'] = $message->to_profile;
945 $dmsg['created_at'] = $this->dateTwitter($message->created);
946 $dmsg['sender_screen_name'] = $from_profile->nickname;
947 $dmsg['recipient_screen_name'] = $to_profile->nickname;
948 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
949 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
954 function rssDirectMessageArray($message)
958 $from = $message->getFrom();
960 $entry['title'] = sprintf('Message from %1$s to %2$s',
961 $from->nickname, $message->getTo()->nickname);
963 $entry['content'] = common_xml_safe_str($message->rendered);
964 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
965 $entry['published'] = common_date_iso8601($message->created);
967 $taguribase = TagURI::base();
969 $entry['id'] = "tag:$taguribase:$entry[link]";
970 $entry['updated'] = $entry['published'];
972 $entry['author-name'] = $from->getBestName();
973 $entry['author-uri'] = $from->homepage;
975 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
977 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
978 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
982 $entry['description'] = $entry['content'];
983 $entry['pubDate'] = common_date_rfc2822($message->created);
984 $entry['guid'] = $entry['link'];
989 function showSingleXmlDirectMessage($message)
991 $this->initDocument('xml');
992 $dmsg = $this->directMessageArray($message);
993 $this->showXmlDirectMessage($dmsg, true);
994 $this->endDocument('xml');
997 function showSingleJsonDirectMessage($message)
999 $this->initDocument('json');
1000 $dmsg = $this->directMessageArray($message);
1001 $this->showJsonObjects($dmsg);
1002 $this->endDocument('json');
1005 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1008 $this->initDocument('atom');
1010 $this->element('title', null, common_xml_safe_str($title));
1011 $this->element('id', null, $id);
1012 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1014 if (!is_null($selfuri)) {
1015 $this->element('link', array('href' => $selfuri,
1016 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1019 $this->element('updated', null, common_date_iso8601('now'));
1020 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1022 if (is_array($group)) {
1023 foreach ($group as $g) {
1024 $this->raw($g->asAtomEntry());
1027 while ($group->fetch()) {
1028 $this->raw($group->asAtomEntry());
1032 $this->endDocument('atom');
1036 function showJsonTimeline($notice)
1039 $this->initDocument('json');
1041 $statuses = array();
1043 if (is_array($notice)) {
1044 $notice = new ArrayWrapper($notice);
1047 while ($notice->fetch()) {
1049 $twitter_status = $this->twitterStatusArray($notice);
1050 array_push($statuses, $twitter_status);
1051 } catch (Exception $e) {
1052 common_log(LOG_ERR, $e->getMessage());
1057 $this->showJsonObjects($statuses);
1059 $this->endDocument('json');
1062 function showJsonGroups($group)
1065 $this->initDocument('json');
1069 if (is_array($group)) {
1070 foreach ($group as $g) {
1071 $twitter_group = $this->twitterGroupArray($g);
1072 array_push($groups, $twitter_group);
1075 while ($group->fetch()) {
1076 $twitter_group = $this->twitterGroupArray($group);
1077 array_push($groups, $twitter_group);
1081 $this->showJsonObjects($groups);
1083 $this->endDocument('json');
1086 function showXmlGroups($group)
1089 $this->initDocument('xml');
1090 $this->elementStart('groups', array('type' => 'array'));
1092 if (is_array($group)) {
1093 foreach ($group as $g) {
1094 $twitter_group = $this->twitterGroupArray($g);
1095 $this->showTwitterXmlGroup($twitter_group);
1098 while ($group->fetch()) {
1099 $twitter_group = $this->twitterGroupArray($group);
1100 $this->showTwitterXmlGroup($twitter_group);
1104 $this->elementEnd('groups');
1105 $this->endDocument('xml');
1108 function showTwitterXmlUsers($user)
1111 $this->initDocument('xml');
1112 $this->elementStart('users', array('type' => 'array',
1113 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1115 if (is_array($user)) {
1116 foreach ($user as $u) {
1117 $twitter_user = $this->twitterUserArray($u);
1118 $this->showTwitterXmlUser($twitter_user);
1121 while ($user->fetch()) {
1122 $twitter_user = $this->twitterUserArray($user);
1123 $this->showTwitterXmlUser($twitter_user);
1127 $this->elementEnd('users');
1128 $this->endDocument('xml');
1131 function showJsonUsers($user)
1134 $this->initDocument('json');
1138 if (is_array($user)) {
1139 foreach ($user as $u) {
1140 $twitter_user = $this->twitterUserArray($u);
1141 array_push($users, $twitter_user);
1144 while ($user->fetch()) {
1145 $twitter_user = $this->twitterUserArray($user);
1146 array_push($users, $twitter_user);
1150 $this->showJsonObjects($users);
1152 $this->endDocument('json');
1155 function showSingleJsonGroup($group)
1157 $this->initDocument('json');
1158 $twitter_group = $this->twitterGroupArray($group);
1159 $this->showJsonObjects($twitter_group);
1160 $this->endDocument('json');
1163 function showSingleXmlGroup($group)
1165 $this->initDocument('xml');
1166 $twitter_group = $this->twitterGroupArray($group);
1167 $this->showTwitterXmlGroup($twitter_group);
1168 $this->endDocument('xml');
1171 function dateTwitter($dt)
1173 $dateStr = date('d F Y H:i:s', strtotime($dt));
1174 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1175 $d->setTimezone(new DateTimeZone(common_timezone()));
1176 return $d->format('D M d H:i:s O Y');
1179 function initDocument($type='xml')
1183 header('Content-Type: application/xml; charset=utf-8');
1187 header('Content-Type: application/json; charset=utf-8');
1189 // Check for JSONP callback
1190 if (isset($this->callback)) {
1191 print $this->callback . '(';
1195 header("Content-Type: application/rss+xml; charset=utf-8");
1196 $this->initTwitterRss();
1199 header('Content-Type: application/atom+xml; charset=utf-8');
1200 $this->initTwitterAtom();
1203 // TRANS: Client error on an API request with an unsupported data format.
1204 $this->clientError(_('Not a supported data format.'));
1211 function endDocument($type='xml')
1219 // Check for JSONP callback
1220 if (isset($this->callback)) {
1225 $this->endTwitterRss();
1228 $this->endTwitterRss();
1231 // TRANS: Client error on an API request with an unsupported data format.
1232 $this->clientError(_('Not a supported data format.'));
1238 function clientError($msg, $code = 400, $format = 'xml')
1240 $action = $this->trimmed('action');
1242 common_debug("User error '$code' on '$action': $msg", __FILE__);
1244 if (!array_key_exists($code, ClientErrorAction::$status)) {
1248 $status_string = ClientErrorAction::$status[$code];
1250 // Do not emit error header for JSONP
1251 if (!isset($this->callback)) {
1252 header('HTTP/1.1 '.$code.' '.$status_string);
1255 if ($format == 'xml') {
1256 $this->initDocument('xml');
1257 $this->elementStart('hash');
1258 $this->element('error', null, $msg);
1259 $this->element('request', null, $_SERVER['REQUEST_URI']);
1260 $this->elementEnd('hash');
1261 $this->endDocument('xml');
1262 } elseif ($format == 'json'){
1263 $this->initDocument('json');
1264 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1265 print(json_encode($error_array));
1266 $this->endDocument('json');
1269 // If user didn't request a useful format, throw a regular client error
1270 throw new ClientException($msg, $code);
1274 function serverError($msg, $code = 500, $content_type = 'xml')
1276 $action = $this->trimmed('action');
1278 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1280 if (!array_key_exists($code, ServerErrorAction::$status)) {
1284 $status_string = ServerErrorAction::$status[$code];
1286 // Do not emit error header for JSONP
1287 if (!isset($this->callback)) {
1288 header('HTTP/1.1 '.$code.' '.$status_string);
1291 if ($content_type == 'xml') {
1292 $this->initDocument('xml');
1293 $this->elementStart('hash');
1294 $this->element('error', null, $msg);
1295 $this->element('request', null, $_SERVER['REQUEST_URI']);
1296 $this->elementEnd('hash');
1297 $this->endDocument('xml');
1299 $this->initDocument('json');
1300 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1301 print(json_encode($error_array));
1302 $this->endDocument('json');
1306 function initTwitterRss()
1309 $this->elementStart(
1313 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1314 'xmlns:georss' => 'http://www.georss.org/georss'
1317 $this->elementStart('channel');
1318 Event::handle('StartApiRss', array($this));
1321 function endTwitterRss()
1323 $this->elementEnd('channel');
1324 $this->elementEnd('rss');
1328 function initTwitterAtom()
1331 // FIXME: don't hardcode the language here!
1332 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1333 'xml:lang' => 'en-US',
1334 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1337 function endTwitterAtom()
1339 $this->elementEnd('feed');
1343 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1345 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1346 switch ($content_type) {
1348 $this->showTwitterXmlUser($profile_array);
1351 $this->showJsonObjects($profile_array);
1354 // TRANS: Client error on an API request with an unsupported data format.
1355 $this->clientError(_('Not a supported data format.'));
1361 function getTargetUser($id)
1365 // Twitter supports these other ways of passing the user ID
1366 if (is_numeric($this->arg('id'))) {
1367 return User::staticGet($this->arg('id'));
1368 } else if ($this->arg('id')) {
1369 $nickname = common_canonical_nickname($this->arg('id'));
1370 return User::staticGet('nickname', $nickname);
1371 } else if ($this->arg('user_id')) {
1372 // This is to ensure that a non-numeric user_id still
1373 // overrides screen_name even if it doesn't get used
1374 if (is_numeric($this->arg('user_id'))) {
1375 return User::staticGet('id', $this->arg('user_id'));
1377 } else if ($this->arg('screen_name')) {
1378 $nickname = common_canonical_nickname($this->arg('screen_name'));
1379 return User::staticGet('nickname', $nickname);
1381 // Fall back to trying the currently authenticated user
1382 return $this->auth_user;
1385 } else if (is_numeric($id)) {
1386 return User::staticGet($id);
1388 $nickname = common_canonical_nickname($id);
1389 return User::staticGet('nickname', $nickname);
1393 function getTargetProfile($id)
1397 // Twitter supports these other ways of passing the user ID
1398 if (is_numeric($this->arg('id'))) {
1399 return Profile::staticGet($this->arg('id'));
1400 } else if ($this->arg('id')) {
1401 $nickname = common_canonical_nickname($this->arg('id'));
1402 return Profile::staticGet('nickname', $nickname);
1403 } else if ($this->arg('user_id')) {
1404 // This is to ensure that a non-numeric user_id still
1405 // overrides screen_name even if it doesn't get used
1406 if (is_numeric($this->arg('user_id'))) {
1407 return Profile::staticGet('id', $this->arg('user_id'));
1409 } else if ($this->arg('screen_name')) {
1410 $nickname = common_canonical_nickname($this->arg('screen_name'));
1411 return Profile::staticGet('nickname', $nickname);
1413 } else if (is_numeric($id)) {
1414 return Profile::staticGet($id);
1416 $nickname = common_canonical_nickname($id);
1417 return Profile::staticGet('nickname', $nickname);
1421 function getTargetGroup($id)
1424 if (is_numeric($this->arg('id'))) {
1425 return User_group::staticGet($this->arg('id'));
1426 } else if ($this->arg('id')) {
1427 $nickname = common_canonical_nickname($this->arg('id'));
1428 $local = Local_group::staticGet('nickname', $nickname);
1429 if (empty($local)) {
1432 return User_group::staticGet('id', $local->id);
1434 } else if ($this->arg('group_id')) {
1435 // This is to ensure that a non-numeric user_id still
1436 // overrides screen_name even if it doesn't get used
1437 if (is_numeric($this->arg('group_id'))) {
1438 return User_group::staticGet('id', $this->arg('group_id'));
1440 } else if ($this->arg('group_name')) {
1441 $nickname = common_canonical_nickname($this->arg('group_name'));
1442 $local = Local_group::staticGet('nickname', $nickname);
1443 if (empty($local)) {
1446 return User_group::staticGet('id', $local->group_id);
1450 } else if (is_numeric($id)) {
1451 return User_group::staticGet($id);
1453 $nickname = common_canonical_nickname($id);
1454 $local = Local_group::staticGet('nickname', $nickname);
1455 if (empty($local)) {
1458 return User_group::staticGet('id', $local->group_id);
1464 * Returns query argument or default value if not found. Certain
1465 * parameters used throughout the API are lightly scrubbed and
1466 * bounds checked. This overrides Action::arg().
1468 * @param string $key requested argument
1469 * @param string $def default value to return if $key is not provided
1473 function arg($key, $def=null)
1476 // XXX: Do even more input validation/scrubbing?
1478 if (array_key_exists($key, $this->args)) {
1481 $page = (int)$this->args['page'];
1482 return ($page < 1) ? 1 : $page;
1484 $count = (int)$this->args['count'];
1487 } elseif ($count > 200) {
1493 $since_id = (int)$this->args['since_id'];
1494 return ($since_id < 1) ? 0 : $since_id;
1496 $max_id = (int)$this->args['max_id'];
1497 return ($max_id < 1) ? 0 : $max_id;
1499 return parent::arg($key, $def);
1507 * Calculate the complete URI that called up this action. Used for
1508 * Atom rel="self" links. Warning: this is funky.
1510 * @return string URL a URL suitable for rel="self" Atom links
1512 function getSelfUri()
1514 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1516 $id = $this->arg('id');
1517 $aargs = array('format' => $this->format);
1522 $tag = $this->arg('tag');
1524 $aargs['tag'] = $tag;
1527 parse_str($_SERVER['QUERY_STRING'], $params);
1529 if (!empty($params)) {
1530 unset($params['p']);
1531 $pstring = http_build_query($params);
1534 $uri = common_local_url($action, $aargs);
1536 if (!empty($pstring)) {
1537 $uri .= '?' . $pstring;