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))) {
466 $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;
526 Event::handle('EndRssEntryArray', array($notice, &$entry));
532 function twitterRelationshipArray($source, $target)
534 $relationship = array();
536 $relationship['source'] =
537 $this->relationshipDetailsArray($source, $target);
538 $relationship['target'] =
539 $this->relationshipDetailsArray($target, $source);
541 return array('relationship' => $relationship);
544 function relationshipDetailsArray($source, $target)
548 $details['screen_name'] = $source->nickname;
549 $details['followed_by'] = $target->isSubscribed($source);
550 $details['following'] = $source->isSubscribed($target);
552 $notifications = false;
554 if ($source->isSubscribed($target)) {
556 $sub = Subscription::pkeyGet(array('subscriber' =>
557 $source->id, 'subscribed' => $target->id));
560 $notifications = ($sub->jabber || $sub->sms);
564 $details['notifications_enabled'] = $notifications;
565 $details['blocking'] = $source->hasBlocked($target);
566 $details['id'] = $source->id;
571 function showTwitterXmlRelationship($relationship)
573 $this->elementStart('relationship');
575 foreach($relationship as $element => $value) {
576 if ($element == 'source' || $element == 'target') {
577 $this->elementStart($element);
578 $this->showXmlRelationshipDetails($value);
579 $this->elementEnd($element);
583 $this->elementEnd('relationship');
586 function showXmlRelationshipDetails($details)
588 foreach($details as $element => $value) {
589 $this->element($element, null, $value);
593 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
597 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
599 $this->elementStart($tag, $attrs);
600 foreach($twitter_status as $element => $value) {
603 $this->showTwitterXmlUser($twitter_status['user']);
606 $this->element($element, null, common_xml_safe_str($value));
609 $this->showXmlAttachments($twitter_status['attachments']);
612 $this->showGeoXML($value);
614 case 'retweeted_status':
615 $this->showTwitterXmlStatus($value, 'retweeted_status');
618 if (strncmp($element, 'statusnet_', 10) == 0) {
619 $this->element('statusnet:'.substr($element, 10), null, $value);
621 $this->element($element, null, $value);
625 $this->elementEnd($tag);
628 function showTwitterXmlGroup($twitter_group)
630 $this->elementStart('group');
631 foreach($twitter_group as $element => $value) {
632 $this->element($element, null, $value);
634 $this->elementEnd('group');
637 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
641 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
643 $this->elementStart($role, $attrs);
644 foreach($twitter_user as $element => $value) {
645 if ($element == 'status') {
646 $this->showTwitterXmlStatus($twitter_user['status']);
647 } else if (strncmp($element, 'statusnet_', 10) == 0) {
648 $this->element('statusnet:'.substr($element, 10), null, $value);
650 $this->element($element, null, $value);
653 $this->elementEnd($role);
656 function showXmlAttachments($attachments) {
657 if (!empty($attachments)) {
658 $this->elementStart('attachments', array('type' => 'array'));
659 foreach ($attachments as $attachment) {
661 $attrs['url'] = $attachment['url'];
662 $attrs['mimetype'] = $attachment['mimetype'];
663 $attrs['size'] = $attachment['size'];
664 $this->element('enclosure', $attrs, '');
666 $this->elementEnd('attachments');
670 function showGeoXML($geo)
674 $this->element('geo');
676 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
677 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
678 $this->elementEnd('geo');
682 function showGeoRSS($geo)
688 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
693 function showTwitterRssItem($entry)
695 $this->elementStart('item');
696 $this->element('title', null, $entry['title']);
697 $this->element('description', null, $entry['description']);
698 $this->element('pubDate', null, $entry['pubDate']);
699 $this->element('guid', null, $entry['guid']);
700 $this->element('link', null, $entry['link']);
702 # RSS only supports 1 enclosure per item
703 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
704 $enclosure = $entry['enclosures'][0];
705 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
708 if(array_key_exists('tags', $entry)){
709 foreach($entry['tags'] as $tag){
710 $this->element('category', null,$tag);
714 $this->showGeoRSS($entry['geo']);
715 $this->elementEnd('item');
718 function showJsonObjects($objects)
720 print(json_encode($objects));
723 function showSingleXmlStatus($notice)
725 $this->initDocument('xml');
726 $twitter_status = $this->twitterStatusArray($notice);
727 $this->showTwitterXmlStatus($twitter_status, 'status', true);
728 $this->endDocument('xml');
731 function show_single_json_status($notice)
733 $this->initDocument('json');
734 $status = $this->twitterStatusArray($notice);
735 $this->showJsonObjects($status);
736 $this->endDocument('json');
739 function showXmlTimeline($notice)
742 $this->initDocument('xml');
743 $this->elementStart('statuses', array('type' => 'array',
744 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
746 if (is_array($notice)) {
747 $notice = new ArrayWrapper($notice);
750 while ($notice->fetch()) {
752 $twitter_status = $this->twitterStatusArray($notice);
753 $this->showTwitterXmlStatus($twitter_status);
754 } catch (Exception $e) {
755 common_log(LOG_ERR, $e->getMessage());
760 $this->elementEnd('statuses');
761 $this->endDocument('xml');
764 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
767 $this->initDocument('rss');
769 $this->element('title', null, $title);
770 $this->element('link', null, $link);
772 if (!is_null($self)) {
776 'type' => 'application/rss+xml',
783 if (!is_null($suplink)) {
784 // For FriendFeed's SUP protocol
785 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
786 'rel' => 'http://api.friendfeed.com/2008/03#sup',
788 'type' => 'application/json'));
791 if (!is_null($logo)) {
792 $this->elementStart('image');
793 $this->element('link', null, $link);
794 $this->element('title', null, $title);
795 $this->element('url', null, $logo);
796 $this->elementEnd('image');
799 $this->element('description', null, $subtitle);
800 $this->element('language', null, 'en-us');
801 $this->element('ttl', null, '40');
803 if (is_array($notice)) {
804 $notice = new ArrayWrapper($notice);
807 while ($notice->fetch()) {
809 $entry = $this->twitterRssEntryArray($notice);
810 $this->showTwitterRssItem($entry);
811 } catch (Exception $e) {
812 common_log(LOG_ERR, $e->getMessage());
813 // continue on exceptions
817 $this->endTwitterRss();
820 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
823 $this->initDocument('atom');
825 $this->element('title', null, $title);
826 $this->element('id', null, $id);
827 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
829 if (!is_null($logo)) {
830 $this->element('logo',null,$logo);
833 if (!is_null($suplink)) {
834 # For FriendFeed's SUP protocol
835 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
837 'type' => 'application/json'));
840 if (!is_null($selfuri)) {
841 $this->element('link', array('href' => $selfuri,
842 'rel' => 'self', 'type' => 'application/atom+xml'), null);
845 $this->element('updated', null, common_date_iso8601('now'));
846 $this->element('subtitle', null, $subtitle);
848 if (is_array($notice)) {
849 $notice = new ArrayWrapper($notice);
852 while ($notice->fetch()) {
854 $this->raw($notice->asAtomEntry());
855 } catch (Exception $e) {
856 common_log(LOG_ERR, $e->getMessage());
861 $this->endDocument('atom');
864 function showRssGroups($group, $title, $link, $subtitle)
867 $this->initDocument('rss');
869 $this->element('title', null, $title);
870 $this->element('link', null, $link);
871 $this->element('description', null, $subtitle);
872 $this->element('language', null, 'en-us');
873 $this->element('ttl', null, '40');
875 if (is_array($group)) {
876 foreach ($group as $g) {
877 $twitter_group = $this->twitterRssGroupArray($g);
878 $this->showTwitterRssItem($twitter_group);
881 while ($group->fetch()) {
882 $twitter_group = $this->twitterRssGroupArray($group);
883 $this->showTwitterRssItem($twitter_group);
887 $this->endTwitterRss();
890 function showTwitterAtomEntry($entry)
892 $this->elementStart('entry');
893 $this->element('title', null, common_xml_safe_str($entry['title']));
896 array('type' => 'html'),
897 common_xml_safe_str($entry['content'])
899 $this->element('id', null, $entry['id']);
900 $this->element('published', null, $entry['published']);
901 $this->element('updated', null, $entry['updated']);
902 $this->element('link', array('type' => 'text/html',
903 'href' => $entry['link'],
904 'rel' => 'alternate'));
905 $this->element('link', array('type' => $entry['avatar-type'],
906 'href' => $entry['avatar'],
908 $this->elementStart('author');
910 $this->element('name', null, $entry['author-name']);
911 $this->element('uri', null, $entry['author-uri']);
913 $this->elementEnd('author');
914 $this->elementEnd('entry');
917 function showXmlDirectMessage($dm, $namespaces=false)
921 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
923 $this->elementStart('direct_message', $attrs);
924 foreach($dm as $element => $value) {
928 $this->showTwitterXmlUser($value, $element);
931 $this->element($element, null, common_xml_safe_str($value));
934 $this->element($element, null, $value);
938 $this->elementEnd('direct_message');
941 function directMessageArray($message)
945 $from_profile = $message->getFrom();
946 $to_profile = $message->getTo();
948 $dmsg['id'] = $message->id;
949 $dmsg['sender_id'] = $message->from_profile;
950 $dmsg['text'] = trim($message->content);
951 $dmsg['recipient_id'] = $message->to_profile;
952 $dmsg['created_at'] = $this->dateTwitter($message->created);
953 $dmsg['sender_screen_name'] = $from_profile->nickname;
954 $dmsg['recipient_screen_name'] = $to_profile->nickname;
955 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
956 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
961 function rssDirectMessageArray($message)
965 $from = $message->getFrom();
967 $entry['title'] = sprintf('Message from %1$s to %2$s',
968 $from->nickname, $message->getTo()->nickname);
970 $entry['content'] = common_xml_safe_str($message->rendered);
971 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
972 $entry['published'] = common_date_iso8601($message->created);
974 $taguribase = TagURI::base();
976 $entry['id'] = "tag:$taguribase:$entry[link]";
977 $entry['updated'] = $entry['published'];
979 $entry['author-name'] = $from->getBestName();
980 $entry['author-uri'] = $from->homepage;
982 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
984 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
985 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
989 $entry['description'] = $entry['content'];
990 $entry['pubDate'] = common_date_rfc2822($message->created);
991 $entry['guid'] = $entry['link'];
996 function showSingleXmlDirectMessage($message)
998 $this->initDocument('xml');
999 $dmsg = $this->directMessageArray($message);
1000 $this->showXmlDirectMessage($dmsg, true);
1001 $this->endDocument('xml');
1004 function showSingleJsonDirectMessage($message)
1006 $this->initDocument('json');
1007 $dmsg = $this->directMessageArray($message);
1008 $this->showJsonObjects($dmsg);
1009 $this->endDocument('json');
1012 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1014 $this->initDocument('atom');
1016 $this->element('title', null, common_xml_safe_str($title));
1017 $this->element('id', null, $id);
1018 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1020 if (!is_null($selfuri)) {
1021 $this->element('link', array('href' => $selfuri,
1022 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1025 $this->element('updated', null, common_date_iso8601('now'));
1026 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1028 if (is_array($group)) {
1029 foreach ($group as $g) {
1030 $this->raw($g->asAtomEntry());
1033 while ($group->fetch()) {
1034 $this->raw($group->asAtomEntry());
1038 $this->endDocument('atom');
1042 function showJsonTimeline($notice)
1044 $this->initDocument('json');
1046 $statuses = array();
1048 if (is_array($notice)) {
1049 $notice = new ArrayWrapper($notice);
1052 while ($notice->fetch()) {
1054 $twitter_status = $this->twitterStatusArray($notice);
1055 array_push($statuses, $twitter_status);
1056 } catch (Exception $e) {
1057 common_log(LOG_ERR, $e->getMessage());
1062 $this->showJsonObjects($statuses);
1064 $this->endDocument('json');
1067 function showJsonGroups($group)
1069 $this->initDocument('json');
1073 if (is_array($group)) {
1074 foreach ($group as $g) {
1075 $twitter_group = $this->twitterGroupArray($g);
1076 array_push($groups, $twitter_group);
1079 while ($group->fetch()) {
1080 $twitter_group = $this->twitterGroupArray($group);
1081 array_push($groups, $twitter_group);
1085 $this->showJsonObjects($groups);
1087 $this->endDocument('json');
1090 function showXmlGroups($group)
1093 $this->initDocument('xml');
1094 $this->elementStart('groups', array('type' => 'array'));
1096 if (is_array($group)) {
1097 foreach ($group as $g) {
1098 $twitter_group = $this->twitterGroupArray($g);
1099 $this->showTwitterXmlGroup($twitter_group);
1102 while ($group->fetch()) {
1103 $twitter_group = $this->twitterGroupArray($group);
1104 $this->showTwitterXmlGroup($twitter_group);
1108 $this->elementEnd('groups');
1109 $this->endDocument('xml');
1112 function showTwitterXmlUsers($user)
1114 $this->initDocument('xml');
1115 $this->elementStart('users', array('type' => 'array',
1116 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1118 if (is_array($user)) {
1119 foreach ($user as $u) {
1120 $twitter_user = $this->twitterUserArray($u);
1121 $this->showTwitterXmlUser($twitter_user);
1124 while ($user->fetch()) {
1125 $twitter_user = $this->twitterUserArray($user);
1126 $this->showTwitterXmlUser($twitter_user);
1130 $this->elementEnd('users');
1131 $this->endDocument('xml');
1134 function showJsonUsers($user)
1136 $this->initDocument('json');
1140 if (is_array($user)) {
1141 foreach ($user as $u) {
1142 $twitter_user = $this->twitterUserArray($u);
1143 array_push($users, $twitter_user);
1146 while ($user->fetch()) {
1147 $twitter_user = $this->twitterUserArray($user);
1148 array_push($users, $twitter_user);
1152 $this->showJsonObjects($users);
1154 $this->endDocument('json');
1157 function showSingleJsonGroup($group)
1159 $this->initDocument('json');
1160 $twitter_group = $this->twitterGroupArray($group);
1161 $this->showJsonObjects($twitter_group);
1162 $this->endDocument('json');
1165 function showSingleXmlGroup($group)
1167 $this->initDocument('xml');
1168 $twitter_group = $this->twitterGroupArray($group);
1169 $this->showTwitterXmlGroup($twitter_group);
1170 $this->endDocument('xml');
1173 function dateTwitter($dt)
1175 $dateStr = date('d F Y H:i:s', strtotime($dt));
1176 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1177 $d->setTimezone(new DateTimeZone(common_timezone()));
1178 return $d->format('D M d H:i:s O Y');
1181 function initDocument($type='xml')
1185 header('Content-Type: application/xml; charset=utf-8');
1189 header('Content-Type: application/json; charset=utf-8');
1191 // Check for JSONP callback
1192 if (isset($this->callback)) {
1193 print $this->callback . '(';
1197 header("Content-Type: application/rss+xml; charset=utf-8");
1198 $this->initTwitterRss();
1201 header('Content-Type: application/atom+xml; charset=utf-8');
1202 $this->initTwitterAtom();
1205 // TRANS: Client error on an API request with an unsupported data format.
1206 $this->clientError(_('Not a supported data format.'));
1213 function endDocument($type='xml')
1220 // Check for JSONP callback
1221 if (isset($this->callback)) {
1226 $this->endTwitterRss();
1229 $this->endTwitterRss();
1232 // TRANS: Client error on an API request with an unsupported data format.
1233 $this->clientError(_('Not a supported data format.'));
1239 function clientError($msg, $code = 400, $format = 'xml')
1241 $action = $this->trimmed('action');
1243 common_debug("User error '$code' on '$action': $msg", __FILE__);
1245 if (!array_key_exists($code, ClientErrorAction::$status)) {
1249 $status_string = ClientErrorAction::$status[$code];
1251 // Do not emit error header for JSONP
1252 if (!isset($this->callback)) {
1253 header('HTTP/1.1 '.$code.' '.$status_string);
1256 if ($format == 'xml') {
1257 $this->initDocument('xml');
1258 $this->elementStart('hash');
1259 $this->element('error', null, $msg);
1260 $this->element('request', null, $_SERVER['REQUEST_URI']);
1261 $this->elementEnd('hash');
1262 $this->endDocument('xml');
1263 } elseif ($format == 'json'){
1264 $this->initDocument('json');
1265 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1266 print(json_encode($error_array));
1267 $this->endDocument('json');
1270 // If user didn't request a useful format, throw a regular client error
1271 throw new ClientException($msg, $code);
1275 function serverError($msg, $code = 500, $content_type = 'xml')
1277 $action = $this->trimmed('action');
1279 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1281 if (!array_key_exists($code, ServerErrorAction::$status)) {
1285 $status_string = ServerErrorAction::$status[$code];
1287 // Do not emit error header for JSONP
1288 if (!isset($this->callback)) {
1289 header('HTTP/1.1 '.$code.' '.$status_string);
1292 if ($content_type == 'xml') {
1293 $this->initDocument('xml');
1294 $this->elementStart('hash');
1295 $this->element('error', null, $msg);
1296 $this->element('request', null, $_SERVER['REQUEST_URI']);
1297 $this->elementEnd('hash');
1298 $this->endDocument('xml');
1300 $this->initDocument('json');
1301 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1302 print(json_encode($error_array));
1303 $this->endDocument('json');
1307 function initTwitterRss()
1310 $this->elementStart(
1314 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1315 'xmlns:georss' => 'http://www.georss.org/georss'
1318 $this->elementStart('channel');
1319 Event::handle('StartApiRss', array($this));
1322 function endTwitterRss()
1324 $this->elementEnd('channel');
1325 $this->elementEnd('rss');
1329 function initTwitterAtom()
1332 // FIXME: don't hardcode the language here!
1333 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1334 'xml:lang' => 'en-US',
1335 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1338 function endTwitterAtom()
1340 $this->elementEnd('feed');
1344 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1346 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1347 switch ($content_type) {
1349 $this->showTwitterXmlUser($profile_array);
1352 $this->showJsonObjects($profile_array);
1355 // TRANS: Client error on an API request with an unsupported data format.
1356 $this->clientError(_('Not a supported data format.'));
1362 function getTargetUser($id)
1366 // Twitter supports these other ways of passing the user ID
1367 if (is_numeric($this->arg('id'))) {
1368 return User::staticGet($this->arg('id'));
1369 } else if ($this->arg('id')) {
1370 $nickname = common_canonical_nickname($this->arg('id'));
1371 return User::staticGet('nickname', $nickname);
1372 } else if ($this->arg('user_id')) {
1373 // This is to ensure that a non-numeric user_id still
1374 // overrides screen_name even if it doesn't get used
1375 if (is_numeric($this->arg('user_id'))) {
1376 return User::staticGet('id', $this->arg('user_id'));
1378 } else if ($this->arg('screen_name')) {
1379 $nickname = common_canonical_nickname($this->arg('screen_name'));
1380 return User::staticGet('nickname', $nickname);
1382 // Fall back to trying the currently authenticated user
1383 return $this->auth_user;
1386 } else if (is_numeric($id)) {
1387 return User::staticGet($id);
1389 $nickname = common_canonical_nickname($id);
1390 return User::staticGet('nickname', $nickname);
1394 function getTargetProfile($id)
1398 // Twitter supports these other ways of passing the user ID
1399 if (is_numeric($this->arg('id'))) {
1400 return Profile::staticGet($this->arg('id'));
1401 } else if ($this->arg('id')) {
1402 $nickname = common_canonical_nickname($this->arg('id'));
1403 return Profile::staticGet('nickname', $nickname);
1404 } else if ($this->arg('user_id')) {
1405 // This is to ensure that a non-numeric user_id still
1406 // overrides screen_name even if it doesn't get used
1407 if (is_numeric($this->arg('user_id'))) {
1408 return Profile::staticGet('id', $this->arg('user_id'));
1410 } else if ($this->arg('screen_name')) {
1411 $nickname = common_canonical_nickname($this->arg('screen_name'));
1412 return Profile::staticGet('nickname', $nickname);
1414 } else if (is_numeric($id)) {
1415 return Profile::staticGet($id);
1417 $nickname = common_canonical_nickname($id);
1418 return Profile::staticGet('nickname', $nickname);
1422 function getTargetGroup($id)
1425 if (is_numeric($this->arg('id'))) {
1426 return User_group::staticGet($this->arg('id'));
1427 } else if ($this->arg('id')) {
1428 $nickname = common_canonical_nickname($this->arg('id'));
1429 $local = Local_group::staticGet('nickname', $nickname);
1430 if (empty($local)) {
1433 return User_group::staticGet('id', $local->id);
1435 } else if ($this->arg('group_id')) {
1436 // This is to ensure that a non-numeric user_id still
1437 // overrides screen_name even if it doesn't get used
1438 if (is_numeric($this->arg('group_id'))) {
1439 return User_group::staticGet('id', $this->arg('group_id'));
1441 } else if ($this->arg('group_name')) {
1442 $nickname = common_canonical_nickname($this->arg('group_name'));
1443 $local = Local_group::staticGet('nickname', $nickname);
1444 if (empty($local)) {
1447 return User_group::staticGet('id', $local->group_id);
1451 } else if (is_numeric($id)) {
1452 return User_group::staticGet($id);
1454 $nickname = common_canonical_nickname($id);
1455 $local = Local_group::staticGet('nickname', $nickname);
1456 if (empty($local)) {
1459 return User_group::staticGet('id', $local->group_id);
1465 * Returns query argument or default value if not found. Certain
1466 * parameters used throughout the API are lightly scrubbed and
1467 * bounds checked. This overrides Action::arg().
1469 * @param string $key requested argument
1470 * @param string $def default value to return if $key is not provided
1474 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;