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')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
108 * @author Craig Andrews <candrews@integralblue.com>
109 * @author Dan Moore <dan@moore.cx>
110 * @author Evan Prodromou <evan@status.net>
111 * @author Jeffery To <jeffery.to@gmail.com>
112 * @author Toby Inkster <mail@tobyinkster.co.uk>
113 * @author Zach Copley <zach@status.net>
114 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115 * @link http://status.net/
118 class ApiAction extends Action
121 const READ_WRITE = 2;
125 var $auth_user = null;
129 var $since_id = null;
131 var $callback = null;
133 var $access = self::READ_ONLY; // read (default) or read-write
135 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
140 * @param array $args Web and URL arguments
142 * @return boolean false if user doesn't exist
145 function prepare($args)
147 StatusNet::setApi(true); // reduce exception reports to aid in debugging
148 parent::prepare($args);
150 $this->format = $this->arg('format');
151 $this->callback = $this->arg('callback');
152 $this->page = (int)$this->arg('page', 1);
153 $this->count = (int)$this->arg('count', 20);
154 $this->max_id = (int)$this->arg('max_id', 0);
155 $this->since_id = (int)$this->arg('since_id', 0);
157 if ($this->arg('since')) {
158 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
161 $this->source = $this->trimmed('source');
163 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
164 $this->source = 'api';
173 * @param array $args Arguments from $_REQUEST
178 function handle($args)
180 header('Access-Control-Allow-Origin: *');
181 parent::handle($args);
185 * Overrides XMLOutputter::element to write booleans as strings (true|false).
186 * See that method's documentation for more info.
188 * @param string $tag Element type or tagname
189 * @param array $attrs Array of element attributes, as
191 * @param string $content string content of the element
195 function element($tag, $attrs=null, $content=null)
197 if (is_bool($content)) {
198 $content = ($content ? 'true' : 'false');
201 return parent::element($tag, $attrs, $content);
204 function twitterUserArray($profile, $get_notice=false)
206 $twitter_user = array();
208 $twitter_user['id'] = intval($profile->id);
209 $twitter_user['name'] = $profile->getBestName();
210 $twitter_user['screen_name'] = $profile->nickname;
211 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
212 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
214 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
215 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
216 Avatar::defaultImage(AVATAR_STREAM_SIZE);
218 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
219 $twitter_user['protected'] = false; # not supported by StatusNet yet
220 $twitter_user['followers_count'] = $profile->subscriberCount();
223 $user = $profile->getUser();
225 // Note: some profiles don't have an associated user
227 $defaultDesign = Design::siteDesign();
230 $design = $user->getDesign();
233 if (empty($design)) {
234 $design = $defaultDesign;
237 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
238 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
240 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
242 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
244 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
245 $twitter_user['profile_sidebar_border_color'] = '';
247 $twitter_user['friends_count'] = $profile->subscriptionCount();
249 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
251 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
255 if (!empty($user) && $user->timezone) {
256 $timezone = $user->timezone;
260 $t->setTimezone(new DateTimeZone($timezone));
262 $twitter_user['utc_offset'] = $t->format('Z');
263 $twitter_user['time_zone'] = $timezone;
265 $twitter_user['profile_background_image_url']
266 = empty($design->backgroundimage)
267 ? '' : ($design->disposition & BACKGROUND_ON)
268 ? Design::url($design->backgroundimage) : '';
270 $twitter_user['profile_background_tile']
271 = empty($design->disposition)
272 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
274 $twitter_user['statuses_count'] = $profile->noticeCount();
276 // Is the requesting user following this user?
277 $twitter_user['following'] = false;
278 $twitter_user['statusnet:blocking'] = false;
279 $twitter_user['notifications'] = false;
281 if (isset($this->auth_user)) {
283 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
284 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
287 $sub = Subscription::pkeyGet(array('subscriber' =>
288 $this->auth_user->id,
289 'subscribed' => $profile->id));
292 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
297 $notice = $profile->getCurrentNotice();
300 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
304 // StatusNet-specific
306 $twitter_user['statusnet:profile_url'] = $profile->profileurl;
308 return $twitter_user;
311 function twitterStatusArray($notice, $include_user=true)
313 $base = $this->twitterSimpleStatusArray($notice, $include_user);
315 if (!empty($notice->repeat_of)) {
316 $original = Notice::staticGet('id', $notice->repeat_of);
317 if (!empty($original)) {
318 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
319 $base['retweeted_status'] = $original_array;
326 function twitterSimpleStatusArray($notice, $include_user=true)
328 $profile = $notice->getProfile();
330 $twitter_status = array();
331 $twitter_status['text'] = $notice->content;
332 $twitter_status['truncated'] = false; # Not possible on StatusNet
333 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
334 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
335 intval($notice->reply_to) : null;
339 $ns = $notice->getSource();
341 if (!empty($ns->name) && !empty($ns->url)) {
342 $source = '<a href="'
343 . htmlspecialchars($ns->url)
344 . '" rel="nofollow">'
345 . htmlspecialchars($ns->name)
352 $twitter_status['source'] = $source;
353 $twitter_status['id'] = intval($notice->id);
355 $replier_profile = null;
357 if ($notice->reply_to) {
358 $reply = Notice::staticGet(intval($notice->reply_to));
360 $replier_profile = $reply->getProfile();
364 $twitter_status['in_reply_to_user_id'] =
365 ($replier_profile) ? intval($replier_profile->id) : null;
366 $twitter_status['in_reply_to_screen_name'] =
367 ($replier_profile) ? $replier_profile->nickname : null;
369 if (isset($notice->lat) && isset($notice->lon)) {
370 // This is the format that GeoJSON expects stuff to be in
371 $twitter_status['geo'] = array('type' => 'Point',
372 'coordinates' => array((float) $notice->lat,
373 (float) $notice->lon));
375 $twitter_status['geo'] = null;
378 if (isset($this->auth_user)) {
379 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
381 $twitter_status['favorited'] = false;
385 $attachments = $notice->attachments();
387 if (!empty($attachments)) {
389 $twitter_status['attachments'] = array();
391 foreach ($attachments as $attachment) {
392 $enclosure_o=$attachment->getEnclosure();
394 $enclosure = array();
395 $enclosure['url'] = $enclosure_o->url;
396 $enclosure['mimetype'] = $enclosure_o->mimetype;
397 $enclosure['size'] = $enclosure_o->size;
398 $twitter_status['attachments'][] = $enclosure;
403 if ($include_user && $profile) {
404 # Don't get notice (recursive!)
405 $twitter_user = $this->twitterUserArray($profile, false);
406 $twitter_status['user'] = $twitter_user;
409 // StatusNet-specific
411 $twitter_status['statusnet:html'] = $notice->rendered;
413 return $twitter_status;
416 function twitterGroupArray($group)
418 $twitter_group = array();
420 $twitter_group['id'] = $group->id;
421 $twitter_group['url'] = $group->permalink();
422 $twitter_group['nickname'] = $group->nickname;
423 $twitter_group['fullname'] = $group->fullname;
425 if (isset($this->auth_user)) {
426 $twitter_group['member'] = $this->auth_user->isMember($group);
427 $twitter_group['blocked'] = Group_block::isBlocked(
429 $this->auth_user->getProfile()
433 $twitter_group['member_count'] = $group->getMemberCount();
434 $twitter_group['original_logo'] = $group->original_logo;
435 $twitter_group['homepage_logo'] = $group->homepage_logo;
436 $twitter_group['stream_logo'] = $group->stream_logo;
437 $twitter_group['mini_logo'] = $group->mini_logo;
438 $twitter_group['homepage'] = $group->homepage;
439 $twitter_group['description'] = $group->description;
440 $twitter_group['location'] = $group->location;
441 $twitter_group['created'] = $this->dateTwitter($group->created);
442 $twitter_group['modified'] = $this->dateTwitter($group->modified);
444 return $twitter_group;
447 function twitterRssGroupArray($group)
450 $entry['content']=$group->description;
451 $entry['title']=$group->nickname;
452 $entry['link']=$group->permalink();
453 $entry['published']=common_date_iso8601($group->created);
454 $entry['updated']==common_date_iso8601($group->modified);
455 $taguribase = common_config('integration', 'groupuri');
456 $entry['id'] = "group:$groupuribase:$entry[link]";
458 $entry['description'] = $entry['content'];
459 $entry['pubDate'] = common_date_rfc2822($group->created);
460 $entry['guid'] = $entry['link'];
465 function twitterRssEntryArray($notice)
467 $profile = $notice->getProfile();
471 // We trim() to avoid extraneous whitespace in the output
473 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
474 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
475 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
476 $entry['published'] = common_date_iso8601($notice->created);
478 $taguribase = TagURI::base();
479 $entry['id'] = "tag:$taguribase:$entry[link]";
481 $entry['updated'] = $entry['published'];
482 $entry['author'] = $profile->getBestName();
485 $attachments = $notice->attachments();
486 $enclosures = array();
488 foreach ($attachments as $attachment) {
489 $enclosure_o=$attachment->getEnclosure();
491 $enclosure = array();
492 $enclosure['url'] = $enclosure_o->url;
493 $enclosure['mimetype'] = $enclosure_o->mimetype;
494 $enclosure['size'] = $enclosure_o->size;
495 $enclosures[] = $enclosure;
499 if (!empty($enclosures)) {
500 $entry['enclosures'] = $enclosures;
504 $tag = new Notice_tag();
505 $tag->notice_id = $notice->id;
507 $entry['tags']=array();
508 while ($tag->fetch()) {
509 $entry['tags'][]=$tag->tag;
515 $entry['description'] = $entry['content'];
516 $entry['pubDate'] = common_date_rfc2822($notice->created);
517 $entry['guid'] = $entry['link'];
519 if (isset($notice->lat) && isset($notice->lon)) {
520 // This is the format that GeoJSON expects stuff to be in.
521 // showGeoRSS() below uses it for XML output, so we reuse it
522 $entry['geo'] = array('type' => 'Point',
523 'coordinates' => array((float) $notice->lat,
524 (float) $notice->lon));
526 $entry['geo'] = null;
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 $this->element($element, null, $value);
621 $this->elementEnd($tag);
624 function showTwitterXmlGroup($twitter_group)
626 $this->elementStart('group');
627 foreach($twitter_group as $element => $value) {
628 $this->element($element, null, $value);
630 $this->elementEnd('group');
633 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
637 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
639 $this->elementStart($role, $attrs);
640 foreach($twitter_user as $element => $value) {
641 if ($element == 'status') {
642 $this->showTwitterXmlStatus($twitter_user['status']);
644 $this->element($element, null, $value);
647 $this->elementEnd($role);
650 function showXmlAttachments($attachments) {
651 if (!empty($attachments)) {
652 $this->elementStart('attachments', array('type' => 'array'));
653 foreach ($attachments as $attachment) {
655 $attrs['url'] = $attachment['url'];
656 $attrs['mimetype'] = $attachment['mimetype'];
657 $attrs['size'] = $attachment['size'];
658 $this->element('enclosure', $attrs, '');
660 $this->elementEnd('attachments');
664 function showGeoXML($geo)
668 $this->element('geo');
670 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
671 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
672 $this->elementEnd('geo');
676 function showGeoRSS($geo)
682 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
687 function showTwitterRssItem($entry)
689 $this->elementStart('item');
690 $this->element('title', null, $entry['title']);
691 $this->element('description', null, $entry['description']);
692 $this->element('pubDate', null, $entry['pubDate']);
693 $this->element('guid', null, $entry['guid']);
694 $this->element('link', null, $entry['link']);
696 # RSS only supports 1 enclosure per item
697 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
698 $enclosure = $entry['enclosures'][0];
699 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
702 if(array_key_exists('tags', $entry)){
703 foreach($entry['tags'] as $tag){
704 $this->element('category', null,$tag);
708 $this->showGeoRSS($entry['geo']);
709 $this->elementEnd('item');
712 function showJsonObjects($objects)
714 print(json_encode($objects));
717 function showSingleXmlStatus($notice)
719 $this->initDocument('xml');
720 $twitter_status = $this->twitterStatusArray($notice);
721 $this->showTwitterXmlStatus($twitter_status, 'status', true);
722 $this->endDocument('xml');
725 function show_single_json_status($notice)
727 $this->initDocument('json');
728 $status = $this->twitterStatusArray($notice);
729 $this->showJsonObjects($status);
730 $this->endDocument('json');
733 function showXmlTimeline($notice)
736 $this->initDocument('xml');
737 $this->elementStart('statuses', array('type' => 'array',
738 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
740 if (is_array($notice)) {
741 $notice = new ArrayWrapper($notice);
744 while ($notice->fetch()) {
746 $twitter_status = $this->twitterStatusArray($notice);
747 $this->showTwitterXmlStatus($twitter_status);
748 } catch (Exception $e) {
749 common_log(LOG_ERR, $e->getMessage());
754 $this->elementEnd('statuses');
755 $this->endDocument('xml');
758 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
761 $this->initDocument('rss');
763 $this->element('title', null, $title);
764 $this->element('link', null, $link);
766 if (!is_null($self)) {
770 'type' => 'application/rss+xml',
777 if (!is_null($suplink)) {
778 // For FriendFeed's SUP protocol
779 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
780 'rel' => 'http://api.friendfeed.com/2008/03#sup',
782 'type' => 'application/json'));
785 if (!is_null($logo)) {
786 $this->elementStart('image');
787 $this->element('link', null, $link);
788 $this->element('title', null, $title);
789 $this->element('url', null, $logo);
790 $this->elementEnd('image');
793 $this->element('description', null, $subtitle);
794 $this->element('language', null, 'en-us');
795 $this->element('ttl', null, '40');
797 if (is_array($notice)) {
798 $notice = new ArrayWrapper($notice);
801 while ($notice->fetch()) {
803 $entry = $this->twitterRssEntryArray($notice);
804 $this->showTwitterRssItem($entry);
805 } catch (Exception $e) {
806 common_log(LOG_ERR, $e->getMessage());
807 // continue on exceptions
811 $this->endTwitterRss();
814 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
817 $this->initDocument('atom');
819 $this->element('title', null, $title);
820 $this->element('id', null, $id);
821 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
823 if (!is_null($logo)) {
824 $this->element('logo',null,$logo);
827 if (!is_null($suplink)) {
828 # For FriendFeed's SUP protocol
829 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
831 'type' => 'application/json'));
834 if (!is_null($selfuri)) {
835 $this->element('link', array('href' => $selfuri,
836 'rel' => 'self', 'type' => 'application/atom+xml'), null);
839 $this->element('updated', null, common_date_iso8601('now'));
840 $this->element('subtitle', null, $subtitle);
842 if (is_array($notice)) {
843 $notice = new ArrayWrapper($notice);
846 while ($notice->fetch()) {
848 $this->raw($notice->asAtomEntry());
849 } catch (Exception $e) {
850 common_log(LOG_ERR, $e->getMessage());
855 $this->endDocument('atom');
859 function showRssGroups($group, $title, $link, $subtitle)
862 $this->initDocument('rss');
864 $this->element('title', null, $title);
865 $this->element('link', null, $link);
866 $this->element('description', null, $subtitle);
867 $this->element('language', null, 'en-us');
868 $this->element('ttl', null, '40');
870 if (is_array($group)) {
871 foreach ($group as $g) {
872 $twitter_group = $this->twitterRssGroupArray($g);
873 $this->showTwitterRssItem($twitter_group);
876 while ($group->fetch()) {
877 $twitter_group = $this->twitterRssGroupArray($group);
878 $this->showTwitterRssItem($twitter_group);
882 $this->endTwitterRss();
885 function showTwitterAtomEntry($entry)
887 $this->elementStart('entry');
888 $this->element('title', null, common_xml_safe_str($entry['title']));
891 array('type' => 'html'),
892 common_xml_safe_str($entry['content'])
894 $this->element('id', null, $entry['id']);
895 $this->element('published', null, $entry['published']);
896 $this->element('updated', null, $entry['updated']);
897 $this->element('link', array('type' => 'text/html',
898 'href' => $entry['link'],
899 'rel' => 'alternate'));
900 $this->element('link', array('type' => $entry['avatar-type'],
901 'href' => $entry['avatar'],
903 $this->elementStart('author');
905 $this->element('name', null, $entry['author-name']);
906 $this->element('uri', null, $entry['author-uri']);
908 $this->elementEnd('author');
909 $this->elementEnd('entry');
912 function showXmlDirectMessage($dm, $namespaces=false)
916 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
918 $this->elementStart('direct_message', $attrs);
919 foreach($dm as $element => $value) {
923 $this->showTwitterXmlUser($value, $element);
926 $this->element($element, null, common_xml_safe_str($value));
929 $this->element($element, null, $value);
933 $this->elementEnd('direct_message');
936 function directMessageArray($message)
940 $from_profile = $message->getFrom();
941 $to_profile = $message->getTo();
943 $dmsg['id'] = $message->id;
944 $dmsg['sender_id'] = $message->from_profile;
945 $dmsg['text'] = trim($message->content);
946 $dmsg['recipient_id'] = $message->to_profile;
947 $dmsg['created_at'] = $this->dateTwitter($message->created);
948 $dmsg['sender_screen_name'] = $from_profile->nickname;
949 $dmsg['recipient_screen_name'] = $to_profile->nickname;
950 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
951 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
956 function rssDirectMessageArray($message)
960 $from = $message->getFrom();
962 $entry['title'] = sprintf('Message from %1$s to %2$s',
963 $from->nickname, $message->getTo()->nickname);
965 $entry['content'] = common_xml_safe_str($message->rendered);
966 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
967 $entry['published'] = common_date_iso8601($message->created);
969 $taguribase = TagURI::base();
971 $entry['id'] = "tag:$taguribase:$entry[link]";
972 $entry['updated'] = $entry['published'];
974 $entry['author-name'] = $from->getBestName();
975 $entry['author-uri'] = $from->homepage;
977 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
979 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
980 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
984 $entry['description'] = $entry['content'];
985 $entry['pubDate'] = common_date_rfc2822($message->created);
986 $entry['guid'] = $entry['link'];
991 function showSingleXmlDirectMessage($message)
993 $this->initDocument('xml');
994 $dmsg = $this->directMessageArray($message);
995 $this->showXmlDirectMessage($dmsg, true);
996 $this->endDocument('xml');
999 function showSingleJsonDirectMessage($message)
1001 $this->initDocument('json');
1002 $dmsg = $this->directMessageArray($message);
1003 $this->showJsonObjects($dmsg);
1004 $this->endDocument('json');
1007 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1010 $this->initDocument('atom');
1012 $this->element('title', null, common_xml_safe_str($title));
1013 $this->element('id', null, $id);
1014 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1016 if (!is_null($selfuri)) {
1017 $this->element('link', array('href' => $selfuri,
1018 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1021 $this->element('updated', null, common_date_iso8601('now'));
1022 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1024 if (is_array($group)) {
1025 foreach ($group as $g) {
1026 $this->raw($g->asAtomEntry());
1029 while ($group->fetch()) {
1030 $this->raw($group->asAtomEntry());
1034 $this->endDocument('atom');
1038 function showJsonTimeline($notice)
1041 $this->initDocument('json');
1043 $statuses = array();
1045 if (is_array($notice)) {
1046 $notice = new ArrayWrapper($notice);
1049 while ($notice->fetch()) {
1051 $twitter_status = $this->twitterStatusArray($notice);
1052 array_push($statuses, $twitter_status);
1053 } catch (Exception $e) {
1054 common_log(LOG_ERR, $e->getMessage());
1059 $this->showJsonObjects($statuses);
1061 $this->endDocument('json');
1064 function showJsonGroups($group)
1067 $this->initDocument('json');
1071 if (is_array($group)) {
1072 foreach ($group as $g) {
1073 $twitter_group = $this->twitterGroupArray($g);
1074 array_push($groups, $twitter_group);
1077 while ($group->fetch()) {
1078 $twitter_group = $this->twitterGroupArray($group);
1079 array_push($groups, $twitter_group);
1083 $this->showJsonObjects($groups);
1085 $this->endDocument('json');
1088 function showXmlGroups($group)
1091 $this->initDocument('xml');
1092 $this->elementStart('groups', array('type' => 'array'));
1094 if (is_array($group)) {
1095 foreach ($group as $g) {
1096 $twitter_group = $this->twitterGroupArray($g);
1097 $this->showTwitterXmlGroup($twitter_group);
1100 while ($group->fetch()) {
1101 $twitter_group = $this->twitterGroupArray($group);
1102 $this->showTwitterXmlGroup($twitter_group);
1106 $this->elementEnd('groups');
1107 $this->endDocument('xml');
1110 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)
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')
1221 // Check for JSONP callback
1222 if (isset($this->callback)) {
1227 $this->endTwitterRss();
1230 $this->endTwitterRss();
1233 // TRANS: Client error on an API request with an unsupported data format.
1234 $this->clientError(_('Not a supported data format.'));
1240 function clientError($msg, $code = 400, $format = 'xml')
1242 $action = $this->trimmed('action');
1244 common_debug("User error '$code' on '$action': $msg", __FILE__);
1246 if (!array_key_exists($code, ClientErrorAction::$status)) {
1250 $status_string = ClientErrorAction::$status[$code];
1252 // Do not emit error header for JSONP
1253 if (!isset($this->callback)) {
1254 header('HTTP/1.1 '.$code.' '.$status_string);
1257 if ($format == 'xml') {
1258 $this->initDocument('xml');
1259 $this->elementStart('hash');
1260 $this->element('error', null, $msg);
1261 $this->element('request', null, $_SERVER['REQUEST_URI']);
1262 $this->elementEnd('hash');
1263 $this->endDocument('xml');
1264 } elseif ($format == 'json'){
1265 $this->initDocument('json');
1266 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1267 print(json_encode($error_array));
1268 $this->endDocument('json');
1271 // If user didn't request a useful format, throw a regular client error
1272 throw new ClientException($msg, $code);
1276 function serverError($msg, $code = 500, $content_type = 'xml')
1278 $action = $this->trimmed('action');
1280 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1282 if (!array_key_exists($code, ServerErrorAction::$status)) {
1286 $status_string = ServerErrorAction::$status[$code];
1288 // Do not emit error header for JSONP
1289 if (!isset($this->callback)) {
1290 header('HTTP/1.1 '.$code.' '.$status_string);
1293 if ($content_type == 'xml') {
1294 $this->initDocument('xml');
1295 $this->elementStart('hash');
1296 $this->element('error', null, $msg);
1297 $this->element('request', null, $_SERVER['REQUEST_URI']);
1298 $this->elementEnd('hash');
1299 $this->endDocument('xml');
1301 $this->initDocument('json');
1302 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1303 print(json_encode($error_array));
1304 $this->endDocument('json');
1308 function initTwitterRss()
1311 $this->elementStart(
1315 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1316 'xmlns:georss' => 'http://www.georss.org/georss'
1319 $this->elementStart('channel');
1320 Event::handle('StartApiRss', array($this));
1323 function endTwitterRss()
1325 $this->elementEnd('channel');
1326 $this->elementEnd('rss');
1330 function initTwitterAtom()
1333 // FIXME: don't hardcode the language here!
1334 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1335 'xml:lang' => 'en-US',
1336 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1339 function endTwitterAtom()
1341 $this->elementEnd('feed');
1345 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1347 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1348 switch ($content_type) {
1350 $this->showTwitterXmlUser($profile_array);
1353 $this->showJsonObjects($profile_array);
1356 // TRANS: Client error on an API request with an unsupported data format.
1357 $this->clientError(_('Not a supported data format.'));
1363 function getTargetUser($id)
1367 // Twitter supports these other ways of passing the user ID
1368 if (is_numeric($this->arg('id'))) {
1369 return User::staticGet($this->arg('id'));
1370 } else if ($this->arg('id')) {
1371 $nickname = common_canonical_nickname($this->arg('id'));
1372 return User::staticGet('nickname', $nickname);
1373 } else if ($this->arg('user_id')) {
1374 // This is to ensure that a non-numeric user_id still
1375 // overrides screen_name even if it doesn't get used
1376 if (is_numeric($this->arg('user_id'))) {
1377 return User::staticGet('id', $this->arg('user_id'));
1379 } else if ($this->arg('screen_name')) {
1380 $nickname = common_canonical_nickname($this->arg('screen_name'));
1381 return User::staticGet('nickname', $nickname);
1383 // Fall back to trying the currently authenticated user
1384 return $this->auth_user;
1387 } else if (is_numeric($id)) {
1388 return User::staticGet($id);
1390 $nickname = common_canonical_nickname($id);
1391 return User::staticGet('nickname', $nickname);
1395 function getTargetProfile($id)
1399 // Twitter supports these other ways of passing the user ID
1400 if (is_numeric($this->arg('id'))) {
1401 return Profile::staticGet($this->arg('id'));
1402 } else if ($this->arg('id')) {
1403 $nickname = common_canonical_nickname($this->arg('id'));
1404 return Profile::staticGet('nickname', $nickname);
1405 } else if ($this->arg('user_id')) {
1406 // This is to ensure that a non-numeric user_id still
1407 // overrides screen_name even if it doesn't get used
1408 if (is_numeric($this->arg('user_id'))) {
1409 return Profile::staticGet('id', $this->arg('user_id'));
1411 } else if ($this->arg('screen_name')) {
1412 $nickname = common_canonical_nickname($this->arg('screen_name'));
1413 return Profile::staticGet('nickname', $nickname);
1415 } else if (is_numeric($id)) {
1416 return Profile::staticGet($id);
1418 $nickname = common_canonical_nickname($id);
1419 return Profile::staticGet('nickname', $nickname);
1423 function getTargetGroup($id)
1426 if (is_numeric($this->arg('id'))) {
1427 return User_group::staticGet($this->arg('id'));
1428 } else if ($this->arg('id')) {
1429 $nickname = common_canonical_nickname($this->arg('id'));
1430 $local = Local_group::staticGet('nickname', $nickname);
1431 if (empty($local)) {
1434 return User_group::staticGet('id', $local->id);
1436 } else if ($this->arg('group_id')) {
1437 // This is to ensure that a non-numeric user_id still
1438 // overrides screen_name even if it doesn't get used
1439 if (is_numeric($this->arg('group_id'))) {
1440 return User_group::staticGet('id', $this->arg('group_id'));
1442 } else if ($this->arg('group_name')) {
1443 $nickname = common_canonical_nickname($this->arg('group_name'));
1444 $local = Local_group::staticGet('nickname', $nickname);
1445 if (empty($local)) {
1448 return User_group::staticGet('id', $local->group_id);
1452 } else if (is_numeric($id)) {
1453 return User_group::staticGet($id);
1455 $nickname = common_canonical_nickname($id);
1456 $local = Local_group::staticGet('nickname', $nickname);
1457 if (empty($local)) {
1460 return User_group::staticGet('id', $local->group_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)
1478 // XXX: Do even more input validation/scrubbing?
1480 if (array_key_exists($key, $this->args)) {
1483 $page = (int)$this->args['page'];
1484 return ($page < 1) ? 1 : $page;
1486 $count = (int)$this->args['count'];
1489 } elseif ($count > 200) {
1495 $since_id = (int)$this->args['since_id'];
1496 return ($since_id < 1) ? 0 : $since_id;
1498 $max_id = (int)$this->args['max_id'];
1499 return ($max_id < 1) ? 0 : $max_id;
1501 return parent::arg($key, $def);
1509 * Calculate the complete URI that called up this action. Used for
1510 * Atom rel="self" links. Warning: this is funky.
1512 * @return string URL a URL suitable for rel="self" Atom links
1514 function getSelfUri()
1516 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1518 $id = $this->arg('id');
1519 $aargs = array('format' => $this->format);
1524 $tag = $this->arg('tag');
1526 $aargs['tag'] = $tag;
1529 parse_str($_SERVER['QUERY_STRING'], $params);
1531 if (!empty($params)) {
1532 unset($params['p']);
1533 $pstring = http_build_query($params);
1536 $uri = common_local_url($action, $aargs);
1538 if (!empty($pstring)) {
1539 $uri .= '?' . $pstring;