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 = (bool)($design->disposition & BACKGROUND_TILE);
268 $twitter_user['statuses_count'] = $profile->noticeCount();
270 // Is the requesting user following this user?
271 $twitter_user['following'] = false;
272 $twitter_user['statusnet:blocking'] = false;
273 $twitter_user['notifications'] = false;
275 if (isset($this->auth_user)) {
277 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
278 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
281 $sub = Subscription::pkeyGet(array('subscriber' =>
282 $this->auth_user->id,
283 'subscribed' => $profile->id));
286 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
291 $notice = $profile->getCurrentNotice();
294 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
298 // StatusNet-specific
300 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
302 return $twitter_user;
305 function twitterStatusArray($notice, $include_user=true)
307 $base = $this->twitterSimpleStatusArray($notice, $include_user);
309 if (!empty($notice->repeat_of)) {
310 $original = Notice::staticGet('id', $notice->repeat_of);
311 if (!empty($original)) {
312 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
313 $base['retweeted_status'] = $original_array;
320 function twitterSimpleStatusArray($notice, $include_user=true)
322 $profile = $notice->getProfile();
324 $twitter_status = array();
325 $twitter_status['text'] = $notice->content;
326 $twitter_status['truncated'] = false; # Not possible on StatusNet
327 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
328 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
329 intval($notice->reply_to) : null;
333 $ns = $notice->getSource();
335 if (!empty($ns->name) && !empty($ns->url)) {
336 $source = '<a href="'
337 . htmlspecialchars($ns->url)
338 . '" rel="nofollow">'
339 . htmlspecialchars($ns->name)
346 $twitter_status['source'] = $source;
347 $twitter_status['id'] = intval($notice->id);
349 $replier_profile = null;
351 if ($notice->reply_to) {
352 $reply = Notice::staticGet(intval($notice->reply_to));
354 $replier_profile = $reply->getProfile();
358 $twitter_status['in_reply_to_user_id'] =
359 ($replier_profile) ? intval($replier_profile->id) : null;
360 $twitter_status['in_reply_to_screen_name'] =
361 ($replier_profile) ? $replier_profile->nickname : null;
363 if (isset($notice->lat) && isset($notice->lon)) {
364 // This is the format that GeoJSON expects stuff to be in
365 $twitter_status['geo'] = array('type' => 'Point',
366 'coordinates' => array((float) $notice->lat,
367 (float) $notice->lon));
369 $twitter_status['geo'] = null;
372 if (isset($this->auth_user)) {
373 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
375 $twitter_status['favorited'] = false;
379 $attachments = $notice->attachments();
381 if (!empty($attachments)) {
383 $twitter_status['attachments'] = array();
385 foreach ($attachments as $attachment) {
386 $enclosure_o=$attachment->getEnclosure();
388 $enclosure = array();
389 $enclosure['url'] = $enclosure_o->url;
390 $enclosure['mimetype'] = $enclosure_o->mimetype;
391 $enclosure['size'] = $enclosure_o->size;
392 $twitter_status['attachments'][] = $enclosure;
397 if ($include_user && $profile) {
398 # Don't get notice (recursive!)
399 $twitter_user = $this->twitterUserArray($profile, false);
400 $twitter_status['user'] = $twitter_user;
403 // StatusNet-specific
405 $twitter_status['statusnet_html'] = $notice->rendered;
407 return $twitter_status;
410 function twitterGroupArray($group)
412 $twitter_group = array();
414 $twitter_group['id'] = intval($group->id);
415 $twitter_group['url'] = $group->permalink();
416 $twitter_group['nickname'] = $group->nickname;
417 $twitter_group['fullname'] = $group->fullname;
419 if (isset($this->auth_user)) {
420 $twitter_group['member'] = $this->auth_user->isMember($group);
421 $twitter_group['blocked'] = Group_block::isBlocked(
423 $this->auth_user->getProfile()
427 $twitter_group['member_count'] = $group->getMemberCount();
428 $twitter_group['original_logo'] = $group->original_logo;
429 $twitter_group['homepage_logo'] = $group->homepage_logo;
430 $twitter_group['stream_logo'] = $group->stream_logo;
431 $twitter_group['mini_logo'] = $group->mini_logo;
432 $twitter_group['homepage'] = $group->homepage;
433 $twitter_group['description'] = $group->description;
434 $twitter_group['location'] = $group->location;
435 $twitter_group['created'] = $this->dateTwitter($group->created);
436 $twitter_group['modified'] = $this->dateTwitter($group->modified);
438 return $twitter_group;
441 function twitterRssGroupArray($group)
444 $entry['content']=$group->description;
445 $entry['title']=$group->nickname;
446 $entry['link']=$group->permalink();
447 $entry['published']=common_date_iso8601($group->created);
448 $entry['updated']==common_date_iso8601($group->modified);
449 $taguribase = common_config('integration', 'groupuri');
450 $entry['id'] = "group:$groupuribase:$entry[link]";
452 $entry['description'] = $entry['content'];
453 $entry['pubDate'] = common_date_rfc2822($group->created);
454 $entry['guid'] = $entry['link'];
459 function twitterRssEntryArray($notice)
463 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
464 $profile = $notice->getProfile();
466 // We trim() to avoid extraneous whitespace in the output
468 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
469 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
470 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
471 $entry['published'] = common_date_iso8601($notice->created);
473 $taguribase = TagURI::base();
474 $entry['id'] = "tag:$taguribase:$entry[link]";
476 $entry['updated'] = $entry['published'];
477 $entry['author'] = $profile->getBestName();
480 $attachments = $notice->attachments();
481 $enclosures = array();
483 foreach ($attachments as $attachment) {
484 $enclosure_o=$attachment->getEnclosure();
486 $enclosure = array();
487 $enclosure['url'] = $enclosure_o->url;
488 $enclosure['mimetype'] = $enclosure_o->mimetype;
489 $enclosure['size'] = $enclosure_o->size;
490 $enclosures[] = $enclosure;
494 if (!empty($enclosures)) {
495 $entry['enclosures'] = $enclosures;
499 $tag = new Notice_tag();
500 $tag->notice_id = $notice->id;
502 $entry['tags']=array();
503 while ($tag->fetch()) {
504 $entry['tags'][]=$tag->tag;
510 $entry['description'] = $entry['content'];
511 $entry['pubDate'] = common_date_rfc2822($notice->created);
512 $entry['guid'] = $entry['link'];
514 if (isset($notice->lat) && isset($notice->lon)) {
515 // This is the format that GeoJSON expects stuff to be in.
516 // showGeoRSS() below uses it for XML output, so we reuse it
517 $entry['geo'] = array('type' => 'Point',
518 'coordinates' => array((float) $notice->lat,
519 (float) $notice->lon));
521 $entry['geo'] = null;
524 Event::handle('EndRssEntryArray', array($notice, &$entry));
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)) {
553 $sub = Subscription::pkeyGet(array('subscriber' =>
554 $source->id, 'subscribed' => $target->id));
557 $notifications = ($sub->jabber || $sub->sms);
561 $details['notifications_enabled'] = $notifications;
562 $details['blocking'] = $source->hasBlocked($target);
563 $details['id'] = intval($source->id);
568 function showTwitterXmlRelationship($relationship)
570 $this->elementStart('relationship');
572 foreach($relationship as $element => $value) {
573 if ($element == 'source' || $element == 'target') {
574 $this->elementStart($element);
575 $this->showXmlRelationshipDetails($value);
576 $this->elementEnd($element);
580 $this->elementEnd('relationship');
583 function showXmlRelationshipDetails($details)
585 foreach($details as $element => $value) {
586 $this->element($element, null, $value);
590 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
594 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
596 $this->elementStart($tag, $attrs);
597 foreach($twitter_status as $element => $value) {
600 $this->showTwitterXmlUser($twitter_status['user']);
603 $this->element($element, null, common_xml_safe_str($value));
606 $this->showXmlAttachments($twitter_status['attachments']);
609 $this->showGeoXML($value);
611 case 'retweeted_status':
612 $this->showTwitterXmlStatus($value, 'retweeted_status');
615 if (strncmp($element, 'statusnet_', 10) == 0) {
616 $this->element('statusnet:'.substr($element, 10), null, $value);
618 $this->element($element, null, $value);
622 $this->elementEnd($tag);
625 function showTwitterXmlGroup($twitter_group)
627 $this->elementStart('group');
628 foreach($twitter_group as $element => $value) {
629 $this->element($element, null, $value);
631 $this->elementEnd('group');
634 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
638 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
640 $this->elementStart($role, $attrs);
641 foreach($twitter_user as $element => $value) {
642 if ($element == 'status') {
643 $this->showTwitterXmlStatus($twitter_user['status']);
644 } else if (strncmp($element, 'statusnet_', 10) == 0) {
645 $this->element('statusnet:'.substr($element, 10), null, $value);
647 $this->element($element, null, $value);
650 $this->elementEnd($role);
653 function showXmlAttachments($attachments) {
654 if (!empty($attachments)) {
655 $this->elementStart('attachments', array('type' => 'array'));
656 foreach ($attachments as $attachment) {
658 $attrs['url'] = $attachment['url'];
659 $attrs['mimetype'] = $attachment['mimetype'];
660 $attrs['size'] = $attachment['size'];
661 $this->element('enclosure', $attrs, '');
663 $this->elementEnd('attachments');
667 function showGeoXML($geo)
671 $this->element('geo');
673 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
674 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
675 $this->elementEnd('geo');
679 function showGeoRSS($geo)
685 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
690 function showTwitterRssItem($entry)
692 $this->elementStart('item');
693 $this->element('title', null, $entry['title']);
694 $this->element('description', null, $entry['description']);
695 $this->element('pubDate', null, $entry['pubDate']);
696 $this->element('guid', null, $entry['guid']);
697 $this->element('link', null, $entry['link']);
699 # RSS only supports 1 enclosure per item
700 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
701 $enclosure = $entry['enclosures'][0];
702 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
705 if(array_key_exists('tags', $entry)){
706 foreach($entry['tags'] as $tag){
707 $this->element('category', null,$tag);
711 $this->showGeoRSS($entry['geo']);
712 $this->elementEnd('item');
715 function showJsonObjects($objects)
717 print(json_encode($objects));
720 function showSingleXmlStatus($notice)
722 $this->initDocument('xml');
723 $twitter_status = $this->twitterStatusArray($notice);
724 $this->showTwitterXmlStatus($twitter_status, 'status', true);
725 $this->endDocument('xml');
728 function showSingleAtomStatus($notice)
730 header('Content-Type: application/atom+xml; charset=utf-8');
731 print $notice->asAtomEntry(true, true, true, $this->auth_user);
734 function show_single_json_status($notice)
736 $this->initDocument('json');
737 $status = $this->twitterStatusArray($notice);
738 $this->showJsonObjects($status);
739 $this->endDocument('json');
742 function showXmlTimeline($notice)
744 $this->initDocument('xml');
745 $this->elementStart('statuses', array('type' => 'array',
746 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
748 if (is_array($notice)) {
749 $notice = new ArrayWrapper($notice);
752 while ($notice->fetch()) {
754 $twitter_status = $this->twitterStatusArray($notice);
755 $this->showTwitterXmlStatus($twitter_status);
756 } catch (Exception $e) {
757 common_log(LOG_ERR, $e->getMessage());
762 $this->elementEnd('statuses');
763 $this->endDocument('xml');
766 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
768 $this->initDocument('rss');
770 $this->element('title', null, $title);
771 $this->element('link', null, $link);
773 if (!is_null($self)) {
777 'type' => 'application/rss+xml',
784 if (!is_null($suplink)) {
785 // For FriendFeed's SUP protocol
786 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
787 'rel' => 'http://api.friendfeed.com/2008/03#sup',
789 'type' => 'application/json'));
792 if (!is_null($logo)) {
793 $this->elementStart('image');
794 $this->element('link', null, $link);
795 $this->element('title', null, $title);
796 $this->element('url', null, $logo);
797 $this->elementEnd('image');
800 $this->element('description', null, $subtitle);
801 $this->element('language', null, 'en-us');
802 $this->element('ttl', null, '40');
804 if (is_array($notice)) {
805 $notice = new ArrayWrapper($notice);
808 while ($notice->fetch()) {
810 $entry = $this->twitterRssEntryArray($notice);
811 $this->showTwitterRssItem($entry);
812 } catch (Exception $e) {
813 common_log(LOG_ERR, $e->getMessage());
814 // continue on exceptions
818 $this->endTwitterRss();
821 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)
866 $this->initDocument('rss');
868 $this->element('title', null, $title);
869 $this->element('link', null, $link);
870 $this->element('description', null, $subtitle);
871 $this->element('language', null, 'en-us');
872 $this->element('ttl', null, '40');
874 if (is_array($group)) {
875 foreach ($group as $g) {
876 $twitter_group = $this->twitterRssGroupArray($g);
877 $this->showTwitterRssItem($twitter_group);
880 while ($group->fetch()) {
881 $twitter_group = $this->twitterRssGroupArray($group);
882 $this->showTwitterRssItem($twitter_group);
886 $this->endTwitterRss();
889 function showTwitterAtomEntry($entry)
891 $this->elementStart('entry');
892 $this->element('title', null, common_xml_safe_str($entry['title']));
895 array('type' => 'html'),
896 common_xml_safe_str($entry['content'])
898 $this->element('id', null, $entry['id']);
899 $this->element('published', null, $entry['published']);
900 $this->element('updated', null, $entry['updated']);
901 $this->element('link', array('type' => 'text/html',
902 'href' => $entry['link'],
903 'rel' => 'alternate'));
904 $this->element('link', array('type' => $entry['avatar-type'],
905 'href' => $entry['avatar'],
907 $this->elementStart('author');
909 $this->element('name', null, $entry['author-name']);
910 $this->element('uri', null, $entry['author-uri']);
912 $this->elementEnd('author');
913 $this->elementEnd('entry');
916 function showXmlDirectMessage($dm, $namespaces=false)
920 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
922 $this->elementStart('direct_message', $attrs);
923 foreach($dm as $element => $value) {
927 $this->showTwitterXmlUser($value, $element);
930 $this->element($element, null, common_xml_safe_str($value));
933 $this->element($element, null, $value);
937 $this->elementEnd('direct_message');
940 function directMessageArray($message)
944 $from_profile = $message->getFrom();
945 $to_profile = $message->getTo();
947 $dmsg['id'] = intval($message->id);
948 $dmsg['sender_id'] = intval($from_profile);
949 $dmsg['text'] = trim($message->content);
950 $dmsg['recipient_id'] = intval($to_profile);
951 $dmsg['created_at'] = $this->dateTwitter($message->created);
952 $dmsg['sender_screen_name'] = $from_profile->nickname;
953 $dmsg['recipient_screen_name'] = $to_profile->nickname;
954 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
955 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
960 function rssDirectMessageArray($message)
964 $from = $message->getFrom();
966 $entry['title'] = sprintf('Message from %1$s to %2$s',
967 $from->nickname, $message->getTo()->nickname);
969 $entry['content'] = common_xml_safe_str($message->rendered);
970 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
971 $entry['published'] = common_date_iso8601($message->created);
973 $taguribase = TagURI::base();
975 $entry['id'] = "tag:$taguribase:$entry[link]";
976 $entry['updated'] = $entry['published'];
978 $entry['author-name'] = $from->getBestName();
979 $entry['author-uri'] = $from->homepage;
981 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
983 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
984 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
988 $entry['description'] = $entry['content'];
989 $entry['pubDate'] = common_date_rfc2822($message->created);
990 $entry['guid'] = $entry['link'];
995 function showSingleXmlDirectMessage($message)
997 $this->initDocument('xml');
998 $dmsg = $this->directMessageArray($message);
999 $this->showXmlDirectMessage($dmsg, true);
1000 $this->endDocument('xml');
1003 function showSingleJsonDirectMessage($message)
1005 $this->initDocument('json');
1006 $dmsg = $this->directMessageArray($message);
1007 $this->showJsonObjects($dmsg);
1008 $this->endDocument('json');
1011 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1013 $this->initDocument('atom');
1015 $this->element('title', null, common_xml_safe_str($title));
1016 $this->element('id', null, $id);
1017 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1019 if (!is_null($selfuri)) {
1020 $this->element('link', array('href' => $selfuri,
1021 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1024 $this->element('updated', null, common_date_iso8601('now'));
1025 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1027 if (is_array($group)) {
1028 foreach ($group as $g) {
1029 $this->raw($g->asAtomEntry());
1032 while ($group->fetch()) {
1033 $this->raw($group->asAtomEntry());
1037 $this->endDocument('atom');
1041 function showJsonTimeline($notice)
1043 $this->initDocument('json');
1045 $statuses = array();
1047 if (is_array($notice)) {
1048 $notice = new ArrayWrapper($notice);
1051 while ($notice->fetch()) {
1053 $twitter_status = $this->twitterStatusArray($notice);
1054 array_push($statuses, $twitter_status);
1055 } catch (Exception $e) {
1056 common_log(LOG_ERR, $e->getMessage());
1061 $this->showJsonObjects($statuses);
1063 $this->endDocument('json');
1066 function showJsonGroups($group)
1068 $this->initDocument('json');
1072 if (is_array($group)) {
1073 foreach ($group as $g) {
1074 $twitter_group = $this->twitterGroupArray($g);
1075 array_push($groups, $twitter_group);
1078 while ($group->fetch()) {
1079 $twitter_group = $this->twitterGroupArray($group);
1080 array_push($groups, $twitter_group);
1084 $this->showJsonObjects($groups);
1086 $this->endDocument('json');
1089 function showXmlGroups($group)
1092 $this->initDocument('xml');
1093 $this->elementStart('groups', array('type' => 'array'));
1095 if (is_array($group)) {
1096 foreach ($group as $g) {
1097 $twitter_group = $this->twitterGroupArray($g);
1098 $this->showTwitterXmlGroup($twitter_group);
1101 while ($group->fetch()) {
1102 $twitter_group = $this->twitterGroupArray($group);
1103 $this->showTwitterXmlGroup($twitter_group);
1107 $this->elementEnd('groups');
1108 $this->endDocument('xml');
1111 function showTwitterXmlUsers($user)
1113 $this->initDocument('xml');
1114 $this->elementStart('users', array('type' => 'array',
1115 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1117 if (is_array($user)) {
1118 foreach ($user as $u) {
1119 $twitter_user = $this->twitterUserArray($u);
1120 $this->showTwitterXmlUser($twitter_user);
1123 while ($user->fetch()) {
1124 $twitter_user = $this->twitterUserArray($user);
1125 $this->showTwitterXmlUser($twitter_user);
1129 $this->elementEnd('users');
1130 $this->endDocument('xml');
1133 function showJsonUsers($user)
1135 $this->initDocument('json');
1139 if (is_array($user)) {
1140 foreach ($user as $u) {
1141 $twitter_user = $this->twitterUserArray($u);
1142 array_push($users, $twitter_user);
1145 while ($user->fetch()) {
1146 $twitter_user = $this->twitterUserArray($user);
1147 array_push($users, $twitter_user);
1151 $this->showJsonObjects($users);
1153 $this->endDocument('json');
1156 function showSingleJsonGroup($group)
1158 $this->initDocument('json');
1159 $twitter_group = $this->twitterGroupArray($group);
1160 $this->showJsonObjects($twitter_group);
1161 $this->endDocument('json');
1164 function showSingleXmlGroup($group)
1166 $this->initDocument('xml');
1167 $twitter_group = $this->twitterGroupArray($group);
1168 $this->showTwitterXmlGroup($twitter_group);
1169 $this->endDocument('xml');
1172 function dateTwitter($dt)
1174 $dateStr = date('d F Y H:i:s', strtotime($dt));
1175 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1176 $d->setTimezone(new DateTimeZone(common_timezone()));
1177 return $d->format('D M d H:i:s O Y');
1180 function initDocument($type='xml')
1184 header('Content-Type: application/xml; charset=utf-8');
1188 header('Content-Type: application/json; charset=utf-8');
1190 // Check for JSONP callback
1191 if (isset($this->callback)) {
1192 print $this->callback . '(';
1196 header("Content-Type: application/rss+xml; charset=utf-8");
1197 $this->initTwitterRss();
1200 header('Content-Type: application/atom+xml; charset=utf-8');
1201 $this->initTwitterAtom();
1204 // TRANS: Client error on an API request with an unsupported data format.
1205 $this->clientError(_('Not a supported data format.'));
1212 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 = null)
1240 $action = $this->trimmed('action');
1241 if ($format === null) {
1242 $format = $this->format;
1245 common_debug("User error '$code' on '$action': $msg", __FILE__);
1247 if (!array_key_exists($code, ClientErrorAction::$status)) {
1251 $status_string = ClientErrorAction::$status[$code];
1253 // Do not emit error header for JSONP
1254 if (!isset($this->callback)) {
1255 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1260 $this->initDocument('xml');
1261 $this->elementStart('hash');
1262 $this->element('error', null, $msg);
1263 $this->element('request', null, $_SERVER['REQUEST_URI']);
1264 $this->elementEnd('hash');
1265 $this->endDocument('xml');
1268 $this->initDocument('json');
1269 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1270 print(json_encode($error_array));
1271 $this->endDocument('json');
1274 header('Content-Type: text/plain; charset=utf-8');
1278 // If user didn't request a useful format, throw a regular client error
1279 throw new ClientException($msg, $code);
1283 function serverError($msg, $code = 500, $content_type = null)
1285 $action = $this->trimmed('action');
1286 if ($content_type === null) {
1287 $content_type = $this->format;
1290 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1292 if (!array_key_exists($code, ServerErrorAction::$status)) {
1296 $status_string = ServerErrorAction::$status[$code];
1298 // Do not emit error header for JSONP
1299 if (!isset($this->callback)) {
1300 header('HTTP/1.1 '.$code.' '.$status_string);
1303 if ($content_type == 'xml') {
1304 $this->initDocument('xml');
1305 $this->elementStart('hash');
1306 $this->element('error', null, $msg);
1307 $this->element('request', null, $_SERVER['REQUEST_URI']);
1308 $this->elementEnd('hash');
1309 $this->endDocument('xml');
1311 $this->initDocument('json');
1312 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1313 print(json_encode($error_array));
1314 $this->endDocument('json');
1318 function initTwitterRss()
1321 $this->elementStart(
1325 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1326 'xmlns:georss' => 'http://www.georss.org/georss'
1329 $this->elementStart('channel');
1330 Event::handle('StartApiRss', array($this));
1333 function endTwitterRss()
1335 $this->elementEnd('channel');
1336 $this->elementEnd('rss');
1340 function initTwitterAtom()
1343 // FIXME: don't hardcode the language here!
1344 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1345 'xml:lang' => 'en-US',
1346 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1349 function endTwitterAtom()
1351 $this->elementEnd('feed');
1355 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1357 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1358 switch ($content_type) {
1360 $this->showTwitterXmlUser($profile_array);
1363 $this->showJsonObjects($profile_array);
1366 // TRANS: Client error on an API request with an unsupported data format.
1367 $this->clientError(_('Not a supported data format.'));
1373 private static function is_decimal($str)
1375 return preg_match('/^[0-9]+$/', $str);
1378 function getTargetUser($id)
1381 // Twitter supports these other ways of passing the user ID
1382 if (self::is_decimal($this->arg('id'))) {
1383 return User::staticGet($this->arg('id'));
1384 } else if ($this->arg('id')) {
1385 $nickname = common_canonical_nickname($this->arg('id'));
1386 return User::staticGet('nickname', $nickname);
1387 } else if ($this->arg('user_id')) {
1388 // This is to ensure that a non-numeric user_id still
1389 // overrides screen_name even if it doesn't get used
1390 if (self::is_decimal($this->arg('user_id'))) {
1391 return User::staticGet('id', $this->arg('user_id'));
1393 } else if ($this->arg('screen_name')) {
1394 $nickname = common_canonical_nickname($this->arg('screen_name'));
1395 return User::staticGet('nickname', $nickname);
1397 // Fall back to trying the currently authenticated user
1398 return $this->auth_user;
1401 } else if (self::is_decimal($id)) {
1402 return User::staticGet($id);
1404 $nickname = common_canonical_nickname($id);
1405 return User::staticGet('nickname', $nickname);
1409 function getTargetProfile($id)
1413 // Twitter supports these other ways of passing the user ID
1414 if (self::is_decimal($this->arg('id'))) {
1415 return Profile::staticGet($this->arg('id'));
1416 } else if ($this->arg('id')) {
1417 // Screen names currently can only uniquely identify a local user.
1418 $nickname = common_canonical_nickname($this->arg('id'));
1419 $user = User::staticGet('nickname', $nickname);
1420 return $user ? $user->getProfile() : null;
1421 } else if ($this->arg('user_id')) {
1422 // This is to ensure that a non-numeric user_id still
1423 // overrides screen_name even if it doesn't get used
1424 if (self::is_decimal($this->arg('user_id'))) {
1425 return Profile::staticGet('id', $this->arg('user_id'));
1427 } else if ($this->arg('screen_name')) {
1428 $nickname = common_canonical_nickname($this->arg('screen_name'));
1429 $user = User::staticGet('nickname', $nickname);
1430 return $user ? $user->getProfile() : null;
1432 } else if (self::is_decimal($id)) {
1433 return Profile::staticGet($id);
1435 $nickname = common_canonical_nickname($id);
1436 $user = User::staticGet('nickname', $nickname);
1437 return $user ? $user->getProfile() : null;
1441 function getTargetGroup($id)
1444 if (self::is_decimal($this->arg('id'))) {
1445 return User_group::staticGet('id', $this->arg('id'));
1446 } else if ($this->arg('id')) {
1447 return User_group::getForNickname($this->arg('id'));
1448 } else if ($this->arg('group_id')) {
1449 // This is to ensure that a non-numeric group_id still
1450 // overrides group_name even if it doesn't get used
1451 if (self::is_decimal($this->arg('group_id'))) {
1452 return User_group::staticGet('id', $this->arg('group_id'));
1454 } else if ($this->arg('group_name')) {
1455 return User_group::getForNickname($this->arg('group_name'));
1458 } else if (self::is_decimal($id)) {
1459 return User_group::staticGet('id', $id);
1461 return User_group::getForNickname($id);
1466 * Returns query argument or default value if not found. Certain
1467 * parameters used throughout the API are lightly scrubbed and
1468 * bounds checked. This overrides Action::arg().
1470 * @param string $key requested argument
1471 * @param string $def default value to return if $key is not provided
1475 function arg($key, $def=null)
1477 // XXX: Do even more input validation/scrubbing?
1479 if (array_key_exists($key, $this->args)) {
1482 $page = (int)$this->args['page'];
1483 return ($page < 1) ? 1 : $page;
1485 $count = (int)$this->args['count'];
1488 } elseif ($count > 200) {
1494 $since_id = (int)$this->args['since_id'];
1495 return ($since_id < 1) ? 0 : $since_id;
1497 $max_id = (int)$this->args['max_id'];
1498 return ($max_id < 1) ? 0 : $max_id;
1500 return parent::arg($key, $def);
1508 * Calculate the complete URI that called up this action. Used for
1509 * Atom rel="self" links. Warning: this is funky.
1511 * @return string URL a URL suitable for rel="self" Atom links
1513 function getSelfUri()
1515 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1517 $id = $this->arg('id');
1518 $aargs = array('format' => $this->format);
1523 $tag = $this->arg('tag');
1525 $aargs['tag'] = $tag;
1528 parse_str($_SERVER['QUERY_STRING'], $params);
1530 if (!empty($params)) {
1531 unset($params['p']);
1532 $pstring = http_build_query($params);
1535 $uri = common_local_url($action, $aargs);
1537 if (!empty($pstring)) {
1538 $uri .= '?' . $pstring;