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-2010 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/
115 class ApiAction extends Action
118 const READ_WRITE = 2;
122 var $auth_user = null;
126 var $since_id = null;
128 var $callback = null;
130 var $access = self::READ_ONLY; // read (default) or read-write
132 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
137 * @param array $args Web and URL arguments
139 * @return boolean false if user doesn't exist
141 function prepare($args)
143 StatusNet::setApi(true); // reduce exception reports to aid in debugging
144 parent::prepare($args);
146 $this->format = $this->arg('format');
147 $this->callback = $this->arg('callback');
148 $this->page = (int)$this->arg('page', 1);
149 $this->count = (int)$this->arg('count', 20);
150 $this->max_id = (int)$this->arg('max_id', 0);
151 $this->since_id = (int)$this->arg('since_id', 0);
153 if ($this->arg('since')) {
154 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
157 $this->source = $this->trimmed('source');
159 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
160 $this->source = 'api';
169 * @param array $args Arguments from $_REQUEST
173 function handle($args)
175 header('Access-Control-Allow-Origin: *');
176 parent::handle($args);
180 * Overrides XMLOutputter::element to write booleans as strings (true|false).
181 * See that method's documentation for more info.
183 * @param string $tag Element type or tagname
184 * @param array $attrs Array of element attributes, as
186 * @param string $content string content of the element
190 function element($tag, $attrs=null, $content=null)
192 if (is_bool($content)) {
193 $content = ($content ? 'true' : 'false');
196 return parent::element($tag, $attrs, $content);
199 function twitterUserArray($profile, $get_notice=false)
201 $twitter_user = array();
203 $twitter_user['id'] = intval($profile->id);
204 $twitter_user['name'] = $profile->getBestName();
205 $twitter_user['screen_name'] = $profile->nickname;
206 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
207 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
209 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
210 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
211 Avatar::defaultImage(AVATAR_STREAM_SIZE);
213 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
214 $twitter_user['protected'] = false; # not supported by StatusNet yet
215 $twitter_user['followers_count'] = $profile->subscriberCount();
218 $user = $profile->getUser();
220 // Note: some profiles don't have an associated user
222 $defaultDesign = Design::siteDesign();
225 $design = $user->getDesign();
228 if (empty($design)) {
229 $design = $defaultDesign;
232 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
233 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
234 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
235 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
236 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
237 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
238 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
239 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
240 $twitter_user['profile_sidebar_border_color'] = '';
242 $twitter_user['friends_count'] = $profile->subscriptionCount();
244 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
246 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
250 if (!empty($user) && $user->timezone) {
251 $timezone = $user->timezone;
255 $t->setTimezone(new DateTimeZone($timezone));
257 $twitter_user['utc_offset'] = $t->format('Z');
258 $twitter_user['time_zone'] = $timezone;
260 $twitter_user['profile_background_image_url']
261 = empty($design->backgroundimage)
262 ? '' : ($design->disposition & BACKGROUND_ON)
263 ? Design::url($design->backgroundimage) : '';
265 $twitter_user['profile_background_tile']
266 = empty($design->disposition)
267 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
269 $twitter_user['statuses_count'] = $profile->noticeCount();
271 // Is the requesting user following this user?
272 $twitter_user['following'] = false;
273 $twitter_user['statusnet:blocking'] = false;
274 $twitter_user['notifications'] = false;
276 if (isset($this->auth_user)) {
278 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
279 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
282 $sub = Subscription::pkeyGet(array('subscriber' =>
283 $this->auth_user->id,
284 'subscribed' => $profile->id));
287 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
292 $notice = $profile->getCurrentNotice();
295 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
299 // StatusNet-specific
301 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
303 return $twitter_user;
306 function twitterStatusArray($notice, $include_user=true)
308 $base = $this->twitterSimpleStatusArray($notice, $include_user);
310 if (!empty($notice->repeat_of)) {
311 $original = Notice::staticGet('id', $notice->repeat_of);
312 if (!empty($original)) {
313 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
314 $base['retweeted_status'] = $original_array;
321 function twitterSimpleStatusArray($notice, $include_user=true)
323 $profile = $notice->getProfile();
325 $twitter_status = array();
326 $twitter_status['text'] = $notice->content;
327 $twitter_status['truncated'] = false; # Not possible on StatusNet
328 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
329 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
330 intval($notice->reply_to) : null;
334 $ns = $notice->getSource();
336 if (!empty($ns->name) && !empty($ns->url)) {
337 $source = '<a href="'
338 . htmlspecialchars($ns->url)
339 . '" rel="nofollow">'
340 . htmlspecialchars($ns->name)
347 $twitter_status['source'] = $source;
348 $twitter_status['id'] = intval($notice->id);
350 $replier_profile = null;
352 if ($notice->reply_to) {
353 $reply = Notice::staticGet(intval($notice->reply_to));
355 $replier_profile = $reply->getProfile();
359 $twitter_status['in_reply_to_user_id'] =
360 ($replier_profile) ? intval($replier_profile->id) : null;
361 $twitter_status['in_reply_to_screen_name'] =
362 ($replier_profile) ? $replier_profile->nickname : null;
364 if (isset($notice->lat) && isset($notice->lon)) {
365 // This is the format that GeoJSON expects stuff to be in
366 $twitter_status['geo'] = array('type' => 'Point',
367 'coordinates' => array((float) $notice->lat,
368 (float) $notice->lon));
370 $twitter_status['geo'] = null;
373 if (isset($this->auth_user)) {
374 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
376 $twitter_status['favorited'] = false;
380 $attachments = $notice->attachments();
382 if (!empty($attachments)) {
384 $twitter_status['attachments'] = array();
386 foreach ($attachments as $attachment) {
387 $enclosure_o=$attachment->getEnclosure();
389 $enclosure = array();
390 $enclosure['url'] = $enclosure_o->url;
391 $enclosure['mimetype'] = $enclosure_o->mimetype;
392 $enclosure['size'] = $enclosure_o->size;
393 $twitter_status['attachments'][] = $enclosure;
398 if ($include_user && $profile) {
399 # Don't get notice (recursive!)
400 $twitter_user = $this->twitterUserArray($profile, false);
401 $twitter_status['user'] = $twitter_user;
404 // StatusNet-specific
406 $twitter_status['statusnet_html'] = $notice->rendered;
408 return $twitter_status;
411 function twitterGroupArray($group)
413 $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;
420 if (isset($this->auth_user)) {
421 $twitter_group['member'] = $this->auth_user->isMember($group);
422 $twitter_group['blocked'] = Group_block::isBlocked(
424 $this->auth_user->getProfile()
428 $twitter_group['member_count'] = $group->getMemberCount();
429 $twitter_group['original_logo'] = $group->original_logo;
430 $twitter_group['homepage_logo'] = $group->homepage_logo;
431 $twitter_group['stream_logo'] = $group->stream_logo;
432 $twitter_group['mini_logo'] = $group->mini_logo;
433 $twitter_group['homepage'] = $group->homepage;
434 $twitter_group['description'] = $group->description;
435 $twitter_group['location'] = $group->location;
436 $twitter_group['created'] = $this->dateTwitter($group->created);
437 $twitter_group['modified'] = $this->dateTwitter($group->modified);
439 return $twitter_group;
442 function twitterRssGroupArray($group)
445 $entry['content']=$group->description;
446 $entry['title']=$group->nickname;
447 $entry['link']=$group->permalink();
448 $entry['published']=common_date_iso8601($group->created);
449 $entry['updated']==common_date_iso8601($group->modified);
450 $taguribase = common_config('integration', 'groupuri');
451 $entry['id'] = "group:$groupuribase:$entry[link]";
453 $entry['description'] = $entry['content'];
454 $entry['pubDate'] = common_date_rfc2822($group->created);
455 $entry['guid'] = $entry['link'];
460 function twitterRssEntryArray($notice)
464 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
465 $profile = $notice->getProfile();
467 // We trim() to avoid extraneous whitespace in the output
469 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
470 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
471 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
472 $entry['published'] = common_date_iso8601($notice->created);
474 $taguribase = TagURI::base();
475 $entry['id'] = "tag:$taguribase:$entry[link]";
477 $entry['updated'] = $entry['published'];
478 $entry['author'] = $profile->getBestName();
481 $attachments = $notice->attachments();
482 $enclosures = array();
484 foreach ($attachments as $attachment) {
485 $enclosure_o=$attachment->getEnclosure();
487 $enclosure = array();
488 $enclosure['url'] = $enclosure_o->url;
489 $enclosure['mimetype'] = $enclosure_o->mimetype;
490 $enclosure['size'] = $enclosure_o->size;
491 $enclosures[] = $enclosure;
495 if (!empty($enclosures)) {
496 $entry['enclosures'] = $enclosures;
500 $tag = new Notice_tag();
501 $tag->notice_id = $notice->id;
503 $entry['tags']=array();
504 while ($tag->fetch()) {
505 $entry['tags'][]=$tag->tag;
511 $entry['description'] = $entry['content'];
512 $entry['pubDate'] = common_date_rfc2822($notice->created);
513 $entry['guid'] = $entry['link'];
515 if (isset($notice->lat) && isset($notice->lon)) {
516 // This is the format that GeoJSON expects stuff to be in.
517 // showGeoRSS() below uses it for XML output, so we reuse it
518 $entry['geo'] = array('type' => 'Point',
519 'coordinates' => array((float) $notice->lat,
520 (float) $notice->lon));
522 $entry['geo'] = null;
525 Event::handle('EndRssEntryArray', array($notice, &$entry));
531 function twitterRelationshipArray($source, $target)
533 $relationship = array();
535 $relationship['source'] =
536 $this->relationshipDetailsArray($source, $target);
537 $relationship['target'] =
538 $this->relationshipDetailsArray($target, $source);
540 return array('relationship' => $relationship);
543 function relationshipDetailsArray($source, $target)
547 $details['screen_name'] = $source->nickname;
548 $details['followed_by'] = $target->isSubscribed($source);
549 $details['following'] = $source->isSubscribed($target);
551 $notifications = false;
553 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 if (strncmp($element, 'statusnet_', 10) == 0) {
617 $this->element('statusnet:'.substr($element, 10), null, $value);
619 $this->element($element, null, $value);
623 $this->elementEnd($tag);
626 function showTwitterXmlGroup($twitter_group)
628 $this->elementStart('group');
629 foreach($twitter_group as $element => $value) {
630 $this->element($element, null, $value);
632 $this->elementEnd('group');
635 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
639 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
641 $this->elementStart($role, $attrs);
642 foreach($twitter_user as $element => $value) {
643 if ($element == 'status') {
644 $this->showTwitterXmlStatus($twitter_user['status']);
645 } else if (strncmp($element, 'statusnet_', 10) == 0) {
646 $this->element('statusnet:'.substr($element, 10), null, $value);
648 $this->element($element, null, $value);
651 $this->elementEnd($role);
654 function showXmlAttachments($attachments) {
655 if (!empty($attachments)) {
656 $this->elementStart('attachments', array('type' => 'array'));
657 foreach ($attachments as $attachment) {
659 $attrs['url'] = $attachment['url'];
660 $attrs['mimetype'] = $attachment['mimetype'];
661 $attrs['size'] = $attachment['size'];
662 $this->element('enclosure', $attrs, '');
664 $this->elementEnd('attachments');
668 function showGeoXML($geo)
672 $this->element('geo');
674 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
675 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
676 $this->elementEnd('geo');
680 function showGeoRSS($geo)
686 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
691 function showTwitterRssItem($entry)
693 $this->elementStart('item');
694 $this->element('title', null, $entry['title']);
695 $this->element('description', null, $entry['description']);
696 $this->element('pubDate', null, $entry['pubDate']);
697 $this->element('guid', null, $entry['guid']);
698 $this->element('link', null, $entry['link']);
700 # RSS only supports 1 enclosure per item
701 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
702 $enclosure = $entry['enclosures'][0];
703 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
706 if(array_key_exists('tags', $entry)){
707 foreach($entry['tags'] as $tag){
708 $this->element('category', null,$tag);
712 $this->showGeoRSS($entry['geo']);
713 $this->elementEnd('item');
716 function showJsonObjects($objects)
718 print(json_encode($objects));
721 function showSingleXmlStatus($notice)
723 $this->initDocument('xml');
724 $twitter_status = $this->twitterStatusArray($notice);
725 $this->showTwitterXmlStatus($twitter_status, 'status', true);
726 $this->endDocument('xml');
729 function show_single_json_status($notice)
731 $this->initDocument('json');
732 $status = $this->twitterStatusArray($notice);
733 $this->showJsonObjects($status);
734 $this->endDocument('json');
737 function showXmlTimeline($notice)
739 $this->initDocument('xml');
740 $this->elementStart('statuses', array('type' => 'array',
741 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
743 if (is_array($notice)) {
744 $notice = new ArrayWrapper($notice);
747 while ($notice->fetch()) {
749 $twitter_status = $this->twitterStatusArray($notice);
750 $this->showTwitterXmlStatus($twitter_status);
751 } catch (Exception $e) {
752 common_log(LOG_ERR, $e->getMessage());
757 $this->elementEnd('statuses');
758 $this->endDocument('xml');
761 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
763 $this->initDocument('rss');
765 $this->element('title', null, $title);
766 $this->element('link', null, $link);
768 if (!is_null($self)) {
772 'type' => 'application/rss+xml',
779 if (!is_null($suplink)) {
780 // For FriendFeed's SUP protocol
781 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
782 'rel' => 'http://api.friendfeed.com/2008/03#sup',
784 'type' => 'application/json'));
787 if (!is_null($logo)) {
788 $this->elementStart('image');
789 $this->element('link', null, $link);
790 $this->element('title', null, $title);
791 $this->element('url', null, $logo);
792 $this->elementEnd('image');
795 $this->element('description', null, $subtitle);
796 $this->element('language', null, 'en-us');
797 $this->element('ttl', null, '40');
799 if (is_array($notice)) {
800 $notice = new ArrayWrapper($notice);
803 while ($notice->fetch()) {
805 $entry = $this->twitterRssEntryArray($notice);
806 $this->showTwitterRssItem($entry);
807 } catch (Exception $e) {
808 common_log(LOG_ERR, $e->getMessage());
809 // continue on exceptions
813 $this->endTwitterRss();
816 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
818 $this->initDocument('atom');
820 $this->element('title', null, $title);
821 $this->element('id', null, $id);
822 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
824 if (!is_null($logo)) {
825 $this->element('logo',null,$logo);
828 if (!is_null($suplink)) {
829 # For FriendFeed's SUP protocol
830 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
832 'type' => 'application/json'));
835 if (!is_null($selfuri)) {
836 $this->element('link', array('href' => $selfuri,
837 'rel' => 'self', 'type' => 'application/atom+xml'), null);
840 $this->element('updated', null, common_date_iso8601('now'));
841 $this->element('subtitle', null, $subtitle);
843 if (is_array($notice)) {
844 $notice = new ArrayWrapper($notice);
847 while ($notice->fetch()) {
849 $this->raw($notice->asAtomEntry());
850 } catch (Exception $e) {
851 common_log(LOG_ERR, $e->getMessage());
856 $this->endDocument('atom');
859 function showRssGroups($group, $title, $link, $subtitle)
861 $this->initDocument('rss');
863 $this->element('title', null, $title);
864 $this->element('link', null, $link);
865 $this->element('description', null, $subtitle);
866 $this->element('language', null, 'en-us');
867 $this->element('ttl', null, '40');
869 if (is_array($group)) {
870 foreach ($group as $g) {
871 $twitter_group = $this->twitterRssGroupArray($g);
872 $this->showTwitterRssItem($twitter_group);
875 while ($group->fetch()) {
876 $twitter_group = $this->twitterRssGroupArray($group);
877 $this->showTwitterRssItem($twitter_group);
881 $this->endTwitterRss();
884 function showTwitterAtomEntry($entry)
886 $this->elementStart('entry');
887 $this->element('title', null, common_xml_safe_str($entry['title']));
890 array('type' => 'html'),
891 common_xml_safe_str($entry['content'])
893 $this->element('id', null, $entry['id']);
894 $this->element('published', null, $entry['published']);
895 $this->element('updated', null, $entry['updated']);
896 $this->element('link', array('type' => 'text/html',
897 'href' => $entry['link'],
898 'rel' => 'alternate'));
899 $this->element('link', array('type' => $entry['avatar-type'],
900 'href' => $entry['avatar'],
902 $this->elementStart('author');
904 $this->element('name', null, $entry['author-name']);
905 $this->element('uri', null, $entry['author-uri']);
907 $this->elementEnd('author');
908 $this->elementEnd('entry');
911 function showXmlDirectMessage($dm, $namespaces=false)
915 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
917 $this->elementStart('direct_message', $attrs);
918 foreach($dm as $element => $value) {
922 $this->showTwitterXmlUser($value, $element);
925 $this->element($element, null, common_xml_safe_str($value));
928 $this->element($element, null, $value);
932 $this->elementEnd('direct_message');
935 function directMessageArray($message)
939 $from_profile = $message->getFrom();
940 $to_profile = $message->getTo();
942 $dmsg['id'] = $message->id;
943 $dmsg['sender_id'] = $message->from_profile;
944 $dmsg['text'] = trim($message->content);
945 $dmsg['recipient_id'] = $message->to_profile;
946 $dmsg['created_at'] = $this->dateTwitter($message->created);
947 $dmsg['sender_screen_name'] = $from_profile->nickname;
948 $dmsg['recipient_screen_name'] = $to_profile->nickname;
949 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
950 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
955 function rssDirectMessageArray($message)
959 $from = $message->getFrom();
961 $entry['title'] = sprintf('Message from %1$s to %2$s',
962 $from->nickname, $message->getTo()->nickname);
964 $entry['content'] = common_xml_safe_str($message->rendered);
965 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
966 $entry['published'] = common_date_iso8601($message->created);
968 $taguribase = TagURI::base();
970 $entry['id'] = "tag:$taguribase:$entry[link]";
971 $entry['updated'] = $entry['published'];
973 $entry['author-name'] = $from->getBestName();
974 $entry['author-uri'] = $from->homepage;
976 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
978 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
979 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
983 $entry['description'] = $entry['content'];
984 $entry['pubDate'] = common_date_rfc2822($message->created);
985 $entry['guid'] = $entry['link'];
990 function showSingleXmlDirectMessage($message)
992 $this->initDocument('xml');
993 $dmsg = $this->directMessageArray($message);
994 $this->showXmlDirectMessage($dmsg, true);
995 $this->endDocument('xml');
998 function showSingleJsonDirectMessage($message)
1000 $this->initDocument('json');
1001 $dmsg = $this->directMessageArray($message);
1002 $this->showJsonObjects($dmsg);
1003 $this->endDocument('json');
1006 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)
1038 $this->initDocument('json');
1040 $statuses = array();
1042 if (is_array($notice)) {
1043 $notice = new ArrayWrapper($notice);
1046 while ($notice->fetch()) {
1048 $twitter_status = $this->twitterStatusArray($notice);
1049 array_push($statuses, $twitter_status);
1050 } catch (Exception $e) {
1051 common_log(LOG_ERR, $e->getMessage());
1056 $this->showJsonObjects($statuses);
1058 $this->endDocument('json');
1061 function showJsonGroups($group)
1063 $this->initDocument('json');
1067 if (is_array($group)) {
1068 foreach ($group as $g) {
1069 $twitter_group = $this->twitterGroupArray($g);
1070 array_push($groups, $twitter_group);
1073 while ($group->fetch()) {
1074 $twitter_group = $this->twitterGroupArray($group);
1075 array_push($groups, $twitter_group);
1079 $this->showJsonObjects($groups);
1081 $this->endDocument('json');
1084 function showXmlGroups($group)
1087 $this->initDocument('xml');
1088 $this->elementStart('groups', array('type' => 'array'));
1090 if (is_array($group)) {
1091 foreach ($group as $g) {
1092 $twitter_group = $this->twitterGroupArray($g);
1093 $this->showTwitterXmlGroup($twitter_group);
1096 while ($group->fetch()) {
1097 $twitter_group = $this->twitterGroupArray($group);
1098 $this->showTwitterXmlGroup($twitter_group);
1102 $this->elementEnd('groups');
1103 $this->endDocument('xml');
1106 function showTwitterXmlUsers($user)
1108 $this->initDocument('xml');
1109 $this->elementStart('users', array('type' => 'array',
1110 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1112 if (is_array($user)) {
1113 foreach ($user as $u) {
1114 $twitter_user = $this->twitterUserArray($u);
1115 $this->showTwitterXmlUser($twitter_user);
1118 while ($user->fetch()) {
1119 $twitter_user = $this->twitterUserArray($user);
1120 $this->showTwitterXmlUser($twitter_user);
1124 $this->elementEnd('users');
1125 $this->endDocument('xml');
1128 function showJsonUsers($user)
1130 $this->initDocument('json');
1134 if (is_array($user)) {
1135 foreach ($user as $u) {
1136 $twitter_user = $this->twitterUserArray($u);
1137 array_push($users, $twitter_user);
1140 while ($user->fetch()) {
1141 $twitter_user = $this->twitterUserArray($user);
1142 array_push($users, $twitter_user);
1146 $this->showJsonObjects($users);
1148 $this->endDocument('json');
1151 function showSingleJsonGroup($group)
1153 $this->initDocument('json');
1154 $twitter_group = $this->twitterGroupArray($group);
1155 $this->showJsonObjects($twitter_group);
1156 $this->endDocument('json');
1159 function showSingleXmlGroup($group)
1161 $this->initDocument('xml');
1162 $twitter_group = $this->twitterGroupArray($group);
1163 $this->showTwitterXmlGroup($twitter_group);
1164 $this->endDocument('xml');
1167 function dateTwitter($dt)
1169 $dateStr = date('d F Y H:i:s', strtotime($dt));
1170 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1171 $d->setTimezone(new DateTimeZone(common_timezone()));
1172 return $d->format('D M d H:i:s O Y');
1175 function initDocument($type='xml')
1179 header('Content-Type: application/xml; charset=utf-8');
1183 header('Content-Type: application/json; charset=utf-8');
1185 // Check for JSONP callback
1186 if (isset($this->callback)) {
1187 print $this->callback . '(';
1191 header("Content-Type: application/rss+xml; charset=utf-8");
1192 $this->initTwitterRss();
1195 header('Content-Type: application/atom+xml; charset=utf-8');
1196 $this->initTwitterAtom();
1199 // TRANS: Client error on an API request with an unsupported data format.
1200 $this->clientError(_('Not a supported data format.'));
1207 function endDocument($type='xml')
1214 // Check for JSONP callback
1215 if (isset($this->callback)) {
1220 $this->endTwitterRss();
1223 $this->endTwitterRss();
1226 // TRANS: Client error on an API request with an unsupported data format.
1227 $this->clientError(_('Not a supported data format.'));
1233 function clientError($msg, $code = 400, $format = 'xml')
1235 $action = $this->trimmed('action');
1237 common_debug("User error '$code' on '$action': $msg", __FILE__);
1239 if (!array_key_exists($code, ClientErrorAction::$status)) {
1243 $status_string = ClientErrorAction::$status[$code];
1245 // Do not emit error header for JSONP
1246 if (!isset($this->callback)) {
1247 header('HTTP/1.1 '.$code.' '.$status_string);
1250 if ($format == 'xml') {
1251 $this->initDocument('xml');
1252 $this->elementStart('hash');
1253 $this->element('error', null, $msg);
1254 $this->element('request', null, $_SERVER['REQUEST_URI']);
1255 $this->elementEnd('hash');
1256 $this->endDocument('xml');
1257 } elseif ($format == 'json'){
1258 $this->initDocument('json');
1259 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1260 print(json_encode($error_array));
1261 $this->endDocument('json');
1264 // If user didn't request a useful format, throw a regular client error
1265 throw new ClientException($msg, $code);
1269 function serverError($msg, $code = 500, $content_type = 'xml')
1271 $action = $this->trimmed('action');
1273 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1275 if (!array_key_exists($code, ServerErrorAction::$status)) {
1279 $status_string = ServerErrorAction::$status[$code];
1281 // Do not emit error header for JSONP
1282 if (!isset($this->callback)) {
1283 header('HTTP/1.1 '.$code.' '.$status_string);
1286 if ($content_type == 'xml') {
1287 $this->initDocument('xml');
1288 $this->elementStart('hash');
1289 $this->element('error', null, $msg);
1290 $this->element('request', null, $_SERVER['REQUEST_URI']);
1291 $this->elementEnd('hash');
1292 $this->endDocument('xml');
1294 $this->initDocument('json');
1295 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1296 print(json_encode($error_array));
1297 $this->endDocument('json');
1301 function initTwitterRss()
1304 $this->elementStart(
1308 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1309 'xmlns:georss' => 'http://www.georss.org/georss'
1312 $this->elementStart('channel');
1313 Event::handle('StartApiRss', array($this));
1316 function endTwitterRss()
1318 $this->elementEnd('channel');
1319 $this->elementEnd('rss');
1323 function initTwitterAtom()
1326 // FIXME: don't hardcode the language here!
1327 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1328 'xml:lang' => 'en-US',
1329 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1332 function endTwitterAtom()
1334 $this->elementEnd('feed');
1338 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1340 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1341 switch ($content_type) {
1343 $this->showTwitterXmlUser($profile_array);
1346 $this->showJsonObjects($profile_array);
1349 // TRANS: Client error on an API request with an unsupported data format.
1350 $this->clientError(_('Not a supported data format.'));
1356 function getTargetUser($id)
1359 // Twitter supports these other ways of passing the user ID
1360 if (is_numeric($this->arg('id'))) {
1361 return User::staticGet($this->arg('id'));
1362 } else if ($this->arg('id')) {
1363 $nickname = common_canonical_nickname($this->arg('id'));
1364 return User::staticGet('nickname', $nickname);
1365 } else if ($this->arg('user_id')) {
1366 // This is to ensure that a non-numeric user_id still
1367 // overrides screen_name even if it doesn't get used
1368 if (is_numeric($this->arg('user_id'))) {
1369 return User::staticGet('id', $this->arg('user_id'));
1371 } else if ($this->arg('screen_name')) {
1372 $nickname = common_canonical_nickname($this->arg('screen_name'));
1373 return User::staticGet('nickname', $nickname);
1375 // Fall back to trying the currently authenticated user
1376 return $this->auth_user;
1379 } else if (is_numeric($id)) {
1380 return User::staticGet($id);
1382 $nickname = common_canonical_nickname($id);
1383 return User::staticGet('nickname', $nickname);
1387 function getTargetProfile($id)
1391 // Twitter supports these other ways of passing the user ID
1392 if (is_numeric($this->arg('id'))) {
1393 return Profile::staticGet($this->arg('id'));
1394 } else if ($this->arg('id')) {
1395 $nickname = common_canonical_nickname($this->arg('id'));
1396 return Profile::staticGet('nickname', $nickname);
1397 } else if ($this->arg('user_id')) {
1398 // This is to ensure that a non-numeric user_id still
1399 // overrides screen_name even if it doesn't get used
1400 if (is_numeric($this->arg('user_id'))) {
1401 return Profile::staticGet('id', $this->arg('user_id'));
1403 } else if ($this->arg('screen_name')) {
1404 $nickname = common_canonical_nickname($this->arg('screen_name'));
1405 return Profile::staticGet('nickname', $nickname);
1407 } else if (is_numeric($id)) {
1408 return Profile::staticGet($id);
1410 $nickname = common_canonical_nickname($id);
1411 return Profile::staticGet('nickname', $nickname);
1415 function getTargetGroup($id)
1418 if (is_numeric($this->arg('id'))) {
1419 return User_group::staticGet($this->arg('id'));
1420 } else if ($this->arg('id')) {
1421 $nickname = common_canonical_nickname($this->arg('id'));
1422 $local = Local_group::staticGet('nickname', $nickname);
1423 if (empty($local)) {
1426 return User_group::staticGet('id', $local->id);
1428 } else if ($this->arg('group_id')) {
1429 // This is to ensure that a non-numeric user_id still
1430 // overrides screen_name even if it doesn't get used
1431 if (is_numeric($this->arg('group_id'))) {
1432 return User_group::staticGet('id', $this->arg('group_id'));
1434 } else if ($this->arg('group_name')) {
1435 $nickname = common_canonical_nickname($this->arg('group_name'));
1436 $local = Local_group::staticGet('nickname', $nickname);
1437 if (empty($local)) {
1440 return User_group::staticGet('id', $local->group_id);
1444 } else if (is_numeric($id)) {
1445 return User_group::staticGet($id);
1447 $nickname = common_canonical_nickname($id);
1448 $local = Local_group::staticGet('nickname', $nickname);
1449 if (empty($local)) {
1452 return User_group::staticGet('id', $local->group_id);
1458 * Returns query argument or default value if not found. Certain
1459 * parameters used throughout the API are lightly scrubbed and
1460 * bounds checked. This overrides Action::arg().
1462 * @param string $key requested argument
1463 * @param string $def default value to return if $key is not provided
1467 function arg($key, $def=null)
1469 // XXX: Do even more input validation/scrubbing?
1471 if (array_key_exists($key, $this->args)) {
1474 $page = (int)$this->args['page'];
1475 return ($page < 1) ? 1 : $page;
1477 $count = (int)$this->args['count'];
1480 } elseif ($count > 200) {
1486 $since_id = (int)$this->args['since_id'];
1487 return ($since_id < 1) ? 0 : $since_id;
1489 $max_id = (int)$this->args['max_id'];
1490 return ($max_id < 1) ? 0 : $max_id;
1492 return parent::arg($key, $def);
1500 * Calculate the complete URI that called up this action. Used for
1501 * Atom rel="self" links. Warning: this is funky.
1503 * @return string URL a URL suitable for rel="self" Atom links
1505 function getSelfUri()
1507 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1509 $id = $this->arg('id');
1510 $aargs = array('format' => $this->format);
1515 $tag = $this->arg('tag');
1517 $aargs['tag'] = $tag;
1520 parse_str($_SERVER['QUERY_STRING'], $params);
1522 if (!empty($params)) {
1523 unset($params['p']);
1524 $pstring = http_build_query($params);
1527 $uri = common_local_url($action, $aargs);
1529 if (!empty($pstring)) {
1530 $uri .= '?' . $pstring;