3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009 StatusNet, Inc.
31 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33 * @link http://status.net/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
77 The URLs for methods referred to in this API documentation are
78 relative to the StatusNet API root. The API root is determined by the
79 site's @b server and @b path variables, which are generally specified
80 in config.php. For example:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
102 * Contains most of the Twitter-compatible API output functions.
106 * @author Craig Andrews <candrews@integralblue.com>
107 * @author Dan Moore <dan@moore.cx>
108 * @author Evan Prodromou <evan@status.net>
109 * @author Jeffery To <jeffery.to@gmail.com>
110 * @author Toby Inkster <mail@tobyinkster.co.uk>
111 * @author Zach Copley <zach@status.net>
112 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
113 * @link http://status.net/
116 class ApiAction extends Action
119 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
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
142 function prepare($args)
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
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
174 function handle($args)
176 header('Access-Control-Allow-Origin: *');
177 parent::handle($args);
181 * Overrides XMLOutputter::element to write booleans as strings (true|false).
182 * See that method's documentation for more info.
184 * @param string $tag Element type or tagname
185 * @param array $attrs Array of element attributes, as
187 * @param string $content string content of the element
191 function element($tag, $attrs=null, $content=null)
193 if (is_bool($content)) {
194 $content = ($content ? 'true' : 'false');
197 return parent::element($tag, $attrs, $content);
200 function twitterUserArray($profile, $get_notice=false)
202 $twitter_user = array();
204 $twitter_user['id'] = intval($profile->id);
205 $twitter_user['name'] = $profile->getBestName();
206 $twitter_user['screen_name'] = $profile->nickname;
207 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
208 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
210 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
211 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
212 Avatar::defaultImage(AVATAR_STREAM_SIZE);
214 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
215 $twitter_user['protected'] = false; # not supported by StatusNet yet
216 $twitter_user['followers_count'] = $profile->subscriberCount();
219 $user = $profile->getUser();
221 // Note: some profiles don't have an associated user
223 $defaultDesign = Design::siteDesign();
226 $design = $user->getDesign();
229 if (empty($design)) {
230 $design = $defaultDesign;
233 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
234 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
235 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
236 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
238 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
240 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $twitter_user['profile_sidebar_border_color'] = '';
243 $twitter_user['friends_count'] = $profile->subscriptionCount();
245 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
247 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
251 if (!empty($user) && $user->timezone) {
252 $timezone = $user->timezone;
256 $t->setTimezone(new DateTimeZone($timezone));
258 $twitter_user['utc_offset'] = $t->format('Z');
259 $twitter_user['time_zone'] = $timezone;
261 $twitter_user['profile_background_image_url']
262 = empty($design->backgroundimage)
263 ? '' : ($design->disposition & BACKGROUND_ON)
264 ? Design::url($design->backgroundimage) : '';
266 $twitter_user['profile_background_tile']
267 = empty($design->disposition)
268 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
270 $twitter_user['statuses_count'] = $profile->noticeCount();
272 // Is the requesting user following this user?
273 $twitter_user['following'] = false;
274 $twitter_user['statusnet:blocking'] = false;
275 $twitter_user['notifications'] = false;
277 if (isset($this->auth_user)) {
279 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
280 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
283 $sub = Subscription::pkeyGet(array('subscriber' =>
284 $this->auth_user->id,
285 'subscribed' => $profile->id));
288 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
293 $notice = $profile->getCurrentNotice();
296 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
300 // StatusNet-specific
302 $twitter_user['statusnet:profile_url'] = $profile->profileurl;
304 return $twitter_user;
307 function twitterStatusArray($notice, $include_user=true)
309 $base = $this->twitterSimpleStatusArray($notice, $include_user);
311 if (!empty($notice->repeat_of)) {
312 $original = Notice::staticGet('id', $notice->repeat_of);
313 if (!empty($original)) {
314 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
315 $base['retweeted_status'] = $original_array;
322 function twitterSimpleStatusArray($notice, $include_user=true)
324 $profile = $notice->getProfile();
326 $twitter_status = array();
327 $twitter_status['text'] = $notice->content;
328 $twitter_status['truncated'] = false; # Not possible on StatusNet
329 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
330 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
331 intval($notice->reply_to) : null;
335 $ns = $notice->getSource();
337 if (!empty($ns->name) && !empty($ns->url)) {
338 $source = '<a href="'
339 . htmlspecialchars($ns->url)
340 . '" rel="nofollow">'
341 . htmlspecialchars($ns->name)
348 $twitter_status['source'] = $source;
349 $twitter_status['id'] = intval($notice->id);
351 $replier_profile = null;
353 if ($notice->reply_to) {
354 $reply = Notice::staticGet(intval($notice->reply_to));
356 $replier_profile = $reply->getProfile();
360 $twitter_status['in_reply_to_user_id'] =
361 ($replier_profile) ? intval($replier_profile->id) : null;
362 $twitter_status['in_reply_to_screen_name'] =
363 ($replier_profile) ? $replier_profile->nickname : null;
365 if (isset($notice->lat) && isset($notice->lon)) {
366 // This is the format that GeoJSON expects stuff to be in
367 $twitter_status['geo'] = array('type' => 'Point',
368 'coordinates' => array((float) $notice->lat,
369 (float) $notice->lon));
371 $twitter_status['geo'] = null;
374 if (isset($this->auth_user)) {
375 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
377 $twitter_status['favorited'] = false;
381 $attachments = $notice->attachments();
383 if (!empty($attachments)) {
385 $twitter_status['attachments'] = array();
387 foreach ($attachments as $attachment) {
388 $enclosure_o=$attachment->getEnclosure();
390 $enclosure = array();
391 $enclosure['url'] = $enclosure_o->url;
392 $enclosure['mimetype'] = $enclosure_o->mimetype;
393 $enclosure['size'] = $enclosure_o->size;
394 $twitter_status['attachments'][] = $enclosure;
399 if ($include_user && $profile) {
400 # Don't get notice (recursive!)
401 $twitter_user = $this->twitterUserArray($profile, false);
402 $twitter_status['user'] = $twitter_user;
405 // StatusNet-specific
407 $twitter_status['statusnet:html'] = $notice->rendered;
409 return $twitter_status;
412 function twitterGroupArray($group)
414 $twitter_group = array();
416 $twitter_group['id'] = $group->id;
417 $twitter_group['url'] = $group->permalink();
418 $twitter_group['nickname'] = $group->nickname;
419 $twitter_group['fullname'] = $group->fullname;
421 if (isset($this->auth_user)) {
422 $twitter_group['member'] = $this->auth_user->isMember($group);
423 $twitter_group['blocked'] = Group_block::isBlocked(
425 $this->auth_user->getProfile()
429 $twitter_group['member_count'] = $group->getMemberCount();
430 $twitter_group['original_logo'] = $group->original_logo;
431 $twitter_group['homepage_logo'] = $group->homepage_logo;
432 $twitter_group['stream_logo'] = $group->stream_logo;
433 $twitter_group['mini_logo'] = $group->mini_logo;
434 $twitter_group['homepage'] = $group->homepage;
435 $twitter_group['description'] = $group->description;
436 $twitter_group['location'] = $group->location;
437 $twitter_group['created'] = $this->dateTwitter($group->created);
438 $twitter_group['modified'] = $this->dateTwitter($group->modified);
440 return $twitter_group;
443 function twitterRssGroupArray($group)
446 $entry['content']=$group->description;
447 $entry['title']=$group->nickname;
448 $entry['link']=$group->permalink();
449 $entry['published']=common_date_iso8601($group->created);
450 $entry['updated']==common_date_iso8601($group->modified);
451 $taguribase = common_config('integration', 'groupuri');
452 $entry['id'] = "group:$groupuribase:$entry[link]";
454 $entry['description'] = $entry['content'];
455 $entry['pubDate'] = common_date_rfc2822($group->created);
456 $entry['guid'] = $entry['link'];
461 function twitterRssEntryArray($notice)
463 $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;
527 function twitterRelationshipArray($source, $target)
529 $relationship = array();
531 $relationship['source'] =
532 $this->relationshipDetailsArray($source, $target);
533 $relationship['target'] =
534 $this->relationshipDetailsArray($target, $source);
536 return array('relationship' => $relationship);
539 function relationshipDetailsArray($source, $target)
543 $details['screen_name'] = $source->nickname;
544 $details['followed_by'] = $target->isSubscribed($source);
545 $details['following'] = $source->isSubscribed($target);
547 $notifications = false;
549 if ($source->isSubscribed($target)) {
551 $sub = Subscription::pkeyGet(array('subscriber' =>
552 $source->id, 'subscribed' => $target->id));
555 $notifications = ($sub->jabber || $sub->sms);
559 $details['notifications_enabled'] = $notifications;
560 $details['blocking'] = $source->hasBlocked($target);
561 $details['id'] = $source->id;
566 function showTwitterXmlRelationship($relationship)
568 $this->elementStart('relationship');
570 foreach($relationship as $element => $value) {
571 if ($element == 'source' || $element == 'target') {
572 $this->elementStart($element);
573 $this->showXmlRelationshipDetails($value);
574 $this->elementEnd($element);
578 $this->elementEnd('relationship');
581 function showXmlRelationshipDetails($details)
583 foreach($details as $element => $value) {
584 $this->element($element, null, $value);
588 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
592 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
594 $this->elementStart($tag, $attrs);
595 foreach($twitter_status as $element => $value) {
598 $this->showTwitterXmlUser($twitter_status['user']);
601 $this->element($element, null, common_xml_safe_str($value));
604 $this->showXmlAttachments($twitter_status['attachments']);
607 $this->showGeoXML($value);
609 case 'retweeted_status':
610 $this->showTwitterXmlStatus($value, 'retweeted_status');
613 $this->element($element, null, $value);
616 $this->elementEnd($tag);
619 function showTwitterXmlGroup($twitter_group)
621 $this->elementStart('group');
622 foreach($twitter_group as $element => $value) {
623 $this->element($element, null, $value);
625 $this->elementEnd('group');
628 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
632 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
634 $this->elementStart($role, $attrs);
635 foreach($twitter_user as $element => $value) {
636 if ($element == 'status') {
637 $this->showTwitterXmlStatus($twitter_user['status']);
639 $this->element($element, null, $value);
642 $this->elementEnd($role);
645 function showXmlAttachments($attachments) {
646 if (!empty($attachments)) {
647 $this->elementStart('attachments', array('type' => 'array'));
648 foreach ($attachments as $attachment) {
650 $attrs['url'] = $attachment['url'];
651 $attrs['mimetype'] = $attachment['mimetype'];
652 $attrs['size'] = $attachment['size'];
653 $this->element('enclosure', $attrs, '');
655 $this->elementEnd('attachments');
659 function showGeoXML($geo)
663 $this->element('geo');
665 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
666 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
667 $this->elementEnd('geo');
671 function showGeoRSS($geo)
677 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
682 function showTwitterRssItem($entry)
684 $this->elementStart('item');
685 $this->element('title', null, $entry['title']);
686 $this->element('description', null, $entry['description']);
687 $this->element('pubDate', null, $entry['pubDate']);
688 $this->element('guid', null, $entry['guid']);
689 $this->element('link', null, $entry['link']);
691 # RSS only supports 1 enclosure per item
692 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
693 $enclosure = $entry['enclosures'][0];
694 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
697 if(array_key_exists('tags', $entry)){
698 foreach($entry['tags'] as $tag){
699 $this->element('category', null,$tag);
703 $this->showGeoRSS($entry['geo']);
704 $this->elementEnd('item');
707 function showJsonObjects($objects)
709 print(json_encode($objects));
712 function showSingleXmlStatus($notice)
714 $this->initDocument('xml');
715 $twitter_status = $this->twitterStatusArray($notice);
716 $this->showTwitterXmlStatus($twitter_status, 'status', true);
717 $this->endDocument('xml');
720 function show_single_json_status($notice)
722 $this->initDocument('json');
723 $status = $this->twitterStatusArray($notice);
724 $this->showJsonObjects($status);
725 $this->endDocument('json');
728 function showXmlTimeline($notice)
731 $this->initDocument('xml');
732 $this->elementStart('statuses', array('type' => 'array',
733 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
735 if (is_array($notice)) {
736 foreach ($notice as $n) {
737 $twitter_status = $this->twitterStatusArray($n);
738 $this->showTwitterXmlStatus($twitter_status);
741 while ($notice->fetch()) {
742 $twitter_status = $this->twitterStatusArray($notice);
743 $this->showTwitterXmlStatus($twitter_status);
747 $this->elementEnd('statuses');
748 $this->endDocument('xml');
751 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
754 $this->initDocument('rss');
756 $this->element('title', null, $title);
757 $this->element('link', null, $link);
759 if (!is_null($self)) {
763 'type' => 'application/rss+xml',
770 if (!is_null($suplink)) {
771 // For FriendFeed's SUP protocol
772 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
773 'rel' => 'http://api.friendfeed.com/2008/03#sup',
775 'type' => 'application/json'));
778 if (!is_null($logo)) {
779 $this->elementStart('image');
780 $this->element('link', null, $link);
781 $this->element('title', null, $title);
782 $this->element('url', null, $logo);
783 $this->elementEnd('image');
786 $this->element('description', null, $subtitle);
787 $this->element('language', null, 'en-us');
788 $this->element('ttl', null, '40');
790 if (is_array($notice)) {
791 foreach ($notice as $n) {
792 $entry = $this->twitterRssEntryArray($n);
793 $this->showTwitterRssItem($entry);
796 while ($notice->fetch()) {
797 $entry = $this->twitterRssEntryArray($notice);
798 $this->showTwitterRssItem($entry);
802 $this->endTwitterRss();
805 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
808 $this->initDocument('atom');
810 $this->element('title', null, $title);
811 $this->element('id', null, $id);
812 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
814 if (!is_null($logo)) {
815 $this->element('logo',null,$logo);
818 if (!is_null($suplink)) {
819 # For FriendFeed's SUP protocol
820 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
822 'type' => 'application/json'));
825 if (!is_null($selfuri)) {
826 $this->element('link', array('href' => $selfuri,
827 'rel' => 'self', 'type' => 'application/atom+xml'), null);
830 $this->element('updated', null, common_date_iso8601('now'));
831 $this->element('subtitle', null, $subtitle);
833 if (is_array($notice)) {
834 foreach ($notice as $n) {
835 $this->raw($n->asAtomEntry());
838 while ($notice->fetch()) {
839 $this->raw($notice->asAtomEntry());
843 $this->endDocument('atom');
847 function showRssGroups($group, $title, $link, $subtitle)
850 $this->initDocument('rss');
852 $this->element('title', null, $title);
853 $this->element('link', null, $link);
854 $this->element('description', null, $subtitle);
855 $this->element('language', null, 'en-us');
856 $this->element('ttl', null, '40');
858 if (is_array($group)) {
859 foreach ($group as $g) {
860 $twitter_group = $this->twitterRssGroupArray($g);
861 $this->showTwitterRssItem($twitter_group);
864 while ($group->fetch()) {
865 $twitter_group = $this->twitterRssGroupArray($group);
866 $this->showTwitterRssItem($twitter_group);
870 $this->endTwitterRss();
873 function showTwitterAtomEntry($entry)
875 $this->elementStart('entry');
876 $this->element('title', null, common_xml_safe_str($entry['title']));
879 array('type' => 'html'),
880 common_xml_safe_str($entry['content'])
882 $this->element('id', null, $entry['id']);
883 $this->element('published', null, $entry['published']);
884 $this->element('updated', null, $entry['updated']);
885 $this->element('link', array('type' => 'text/html',
886 'href' => $entry['link'],
887 'rel' => 'alternate'));
888 $this->element('link', array('type' => $entry['avatar-type'],
889 'href' => $entry['avatar'],
891 $this->elementStart('author');
893 $this->element('name', null, $entry['author-name']);
894 $this->element('uri', null, $entry['author-uri']);
896 $this->elementEnd('author');
897 $this->elementEnd('entry');
900 function showXmlDirectMessage($dm, $namespaces=false)
904 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
906 $this->elementStart('direct_message', $attrs);
907 foreach($dm as $element => $value) {
911 $this->showTwitterXmlUser($value, $element);
914 $this->element($element, null, common_xml_safe_str($value));
917 $this->element($element, null, $value);
921 $this->elementEnd('direct_message');
924 function directMessageArray($message)
928 $from_profile = $message->getFrom();
929 $to_profile = $message->getTo();
931 $dmsg['id'] = $message->id;
932 $dmsg['sender_id'] = $message->from_profile;
933 $dmsg['text'] = trim($message->content);
934 $dmsg['recipient_id'] = $message->to_profile;
935 $dmsg['created_at'] = $this->dateTwitter($message->created);
936 $dmsg['sender_screen_name'] = $from_profile->nickname;
937 $dmsg['recipient_screen_name'] = $to_profile->nickname;
938 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
939 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
944 function rssDirectMessageArray($message)
948 $from = $message->getFrom();
950 $entry['title'] = sprintf('Message from %1$s to %2$s',
951 $from->nickname, $message->getTo()->nickname);
953 $entry['content'] = common_xml_safe_str($message->rendered);
954 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
955 $entry['published'] = common_date_iso8601($message->created);
957 $taguribase = TagURI::base();
959 $entry['id'] = "tag:$taguribase:$entry[link]";
960 $entry['updated'] = $entry['published'];
962 $entry['author-name'] = $from->getBestName();
963 $entry['author-uri'] = $from->homepage;
965 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
967 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
968 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
972 $entry['description'] = $entry['content'];
973 $entry['pubDate'] = common_date_rfc2822($message->created);
974 $entry['guid'] = $entry['link'];
979 function showSingleXmlDirectMessage($message)
981 $this->initDocument('xml');
982 $dmsg = $this->directMessageArray($message);
983 $this->showXmlDirectMessage($dmsg, true);
984 $this->endDocument('xml');
987 function showSingleJsonDirectMessage($message)
989 $this->initDocument('json');
990 $dmsg = $this->directMessageArray($message);
991 $this->showJsonObjects($dmsg);
992 $this->endDocument('json');
995 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
998 $this->initDocument('atom');
1000 $this->element('title', null, common_xml_safe_str($title));
1001 $this->element('id', null, $id);
1002 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1004 if (!is_null($selfuri)) {
1005 $this->element('link', array('href' => $selfuri,
1006 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1009 $this->element('updated', null, common_date_iso8601('now'));
1010 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1012 if (is_array($group)) {
1013 foreach ($group as $g) {
1014 $this->raw($g->asAtomEntry());
1017 while ($group->fetch()) {
1018 $this->raw($group->asAtomEntry());
1022 $this->endDocument('atom');
1026 function showJsonTimeline($notice)
1029 $this->initDocument('json');
1031 $statuses = array();
1033 if (is_array($notice)) {
1034 foreach ($notice as $n) {
1035 $twitter_status = $this->twitterStatusArray($n);
1036 array_push($statuses, $twitter_status);
1039 while ($notice->fetch()) {
1040 $twitter_status = $this->twitterStatusArray($notice);
1041 array_push($statuses, $twitter_status);
1045 $this->showJsonObjects($statuses);
1047 $this->endDocument('json');
1050 function showJsonGroups($group)
1053 $this->initDocument('json');
1057 if (is_array($group)) {
1058 foreach ($group as $g) {
1059 $twitter_group = $this->twitterGroupArray($g);
1060 array_push($groups, $twitter_group);
1063 while ($group->fetch()) {
1064 $twitter_group = $this->twitterGroupArray($group);
1065 array_push($groups, $twitter_group);
1069 $this->showJsonObjects($groups);
1071 $this->endDocument('json');
1074 function showXmlGroups($group)
1077 $this->initDocument('xml');
1078 $this->elementStart('groups', array('type' => 'array'));
1080 if (is_array($group)) {
1081 foreach ($group as $g) {
1082 $twitter_group = $this->twitterGroupArray($g);
1083 $this->showTwitterXmlGroup($twitter_group);
1086 while ($group->fetch()) {
1087 $twitter_group = $this->twitterGroupArray($group);
1088 $this->showTwitterXmlGroup($twitter_group);
1092 $this->elementEnd('groups');
1093 $this->endDocument('xml');
1096 function showTwitterXmlUsers($user)
1099 $this->initDocument('xml');
1100 $this->elementStart('users', array('type' => 'array',
1101 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1103 if (is_array($user)) {
1104 foreach ($user as $u) {
1105 $twitter_user = $this->twitterUserArray($u);
1106 $this->showTwitterXmlUser($twitter_user);
1109 while ($user->fetch()) {
1110 $twitter_user = $this->twitterUserArray($user);
1111 $this->showTwitterXmlUser($twitter_user);
1115 $this->elementEnd('users');
1116 $this->endDocument('xml');
1119 function showJsonUsers($user)
1122 $this->initDocument('json');
1126 if (is_array($user)) {
1127 foreach ($user as $u) {
1128 $twitter_user = $this->twitterUserArray($u);
1129 array_push($users, $twitter_user);
1132 while ($user->fetch()) {
1133 $twitter_user = $this->twitterUserArray($user);
1134 array_push($users, $twitter_user);
1138 $this->showJsonObjects($users);
1140 $this->endDocument('json');
1143 function showSingleJsonGroup($group)
1145 $this->initDocument('json');
1146 $twitter_group = $this->twitterGroupArray($group);
1147 $this->showJsonObjects($twitter_group);
1148 $this->endDocument('json');
1151 function showSingleXmlGroup($group)
1153 $this->initDocument('xml');
1154 $twitter_group = $this->twitterGroupArray($group);
1155 $this->showTwitterXmlGroup($twitter_group);
1156 $this->endDocument('xml');
1159 function dateTwitter($dt)
1161 $dateStr = date('d F Y H:i:s', strtotime($dt));
1162 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1163 $d->setTimezone(new DateTimeZone(common_timezone()));
1164 return $d->format('D M d H:i:s O Y');
1167 function initDocument($type='xml')
1171 header('Content-Type: application/xml; charset=utf-8');
1175 header('Content-Type: application/json; charset=utf-8');
1177 // Check for JSONP callback
1178 $callback = $this->arg('callback');
1180 print $callback . '(';
1184 header("Content-Type: application/rss+xml; charset=utf-8");
1185 $this->initTwitterRss();
1188 header('Content-Type: application/atom+xml; charset=utf-8');
1189 $this->initTwitterAtom();
1192 // TRANS: Client error on an API request with an unsupported data format.
1193 $this->clientError(_('Not a supported data format.'));
1200 function endDocument($type='xml')
1208 // Check for JSONP callback
1209 $callback = $this->arg('callback');
1215 $this->endTwitterRss();
1218 $this->endTwitterRss();
1221 // TRANS: Client error on an API request with an unsupported data format.
1222 $this->clientError(_('Not a supported data format.'));
1228 function clientError($msg, $code = 400, $format = 'xml')
1230 $action = $this->trimmed('action');
1232 common_debug("User error '$code' on '$action': $msg", __FILE__);
1234 if (!array_key_exists($code, ClientErrorAction::$status)) {
1238 $status_string = ClientErrorAction::$status[$code];
1240 header('HTTP/1.1 '.$code.' '.$status_string);
1242 if ($format == 'xml') {
1243 $this->initDocument('xml');
1244 $this->elementStart('hash');
1245 $this->element('error', null, $msg);
1246 $this->element('request', null, $_SERVER['REQUEST_URI']);
1247 $this->elementEnd('hash');
1248 $this->endDocument('xml');
1249 } elseif ($format == 'json'){
1250 $this->initDocument('json');
1251 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1252 print(json_encode($error_array));
1253 $this->endDocument('json');
1256 // If user didn't request a useful format, throw a regular client error
1257 throw new ClientException($msg, $code);
1261 function serverError($msg, $code = 500, $content_type = 'xml')
1263 $action = $this->trimmed('action');
1265 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1267 if (!array_key_exists($code, ServerErrorAction::$status)) {
1271 $status_string = ServerErrorAction::$status[$code];
1273 header('HTTP/1.1 '.$code.' '.$status_string);
1275 if ($content_type == 'xml') {
1276 $this->initDocument('xml');
1277 $this->elementStart('hash');
1278 $this->element('error', null, $msg);
1279 $this->element('request', null, $_SERVER['REQUEST_URI']);
1280 $this->elementEnd('hash');
1281 $this->endDocument('xml');
1283 $this->initDocument('json');
1284 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1285 print(json_encode($error_array));
1286 $this->endDocument('json');
1290 function initTwitterRss()
1293 $this->elementStart(
1297 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1298 'xmlns:georss' => 'http://www.georss.org/georss'
1301 $this->elementStart('channel');
1302 Event::handle('StartApiRss', array($this));
1305 function endTwitterRss()
1307 $this->elementEnd('channel');
1308 $this->elementEnd('rss');
1312 function initTwitterAtom()
1315 // FIXME: don't hardcode the language here!
1316 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1317 'xml:lang' => 'en-US',
1318 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1321 function endTwitterAtom()
1323 $this->elementEnd('feed');
1327 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1329 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1330 switch ($content_type) {
1332 $this->showTwitterXmlUser($profile_array);
1335 $this->showJsonObjects($profile_array);
1338 // TRANS: Client error on an API request with an unsupported data format.
1339 $this->clientError(_('Not a supported data format.'));
1345 function getTargetUser($id)
1349 // Twitter supports these other ways of passing the user ID
1350 if (is_numeric($this->arg('id'))) {
1351 return User::staticGet($this->arg('id'));
1352 } else if ($this->arg('id')) {
1353 $nickname = common_canonical_nickname($this->arg('id'));
1354 return User::staticGet('nickname', $nickname);
1355 } else if ($this->arg('user_id')) {
1356 // This is to ensure that a non-numeric user_id still
1357 // overrides screen_name even if it doesn't get used
1358 if (is_numeric($this->arg('user_id'))) {
1359 return User::staticGet('id', $this->arg('user_id'));
1361 } else if ($this->arg('screen_name')) {
1362 $nickname = common_canonical_nickname($this->arg('screen_name'));
1363 return User::staticGet('nickname', $nickname);
1365 // Fall back to trying the currently authenticated user
1366 return $this->auth_user;
1369 } else if (is_numeric($id)) {
1370 return User::staticGet($id);
1372 $nickname = common_canonical_nickname($id);
1373 return User::staticGet('nickname', $nickname);
1377 function getTargetProfile($id)
1381 // Twitter supports these other ways of passing the user ID
1382 if (is_numeric($this->arg('id'))) {
1383 return Profile::staticGet($this->arg('id'));
1384 } else if ($this->arg('id')) {
1385 $nickname = common_canonical_nickname($this->arg('id'));
1386 return Profile::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 (is_numeric($this->arg('user_id'))) {
1391 return Profile::staticGet('id', $this->arg('user_id'));
1393 } else if ($this->arg('screen_name')) {
1394 $nickname = common_canonical_nickname($this->arg('screen_name'));
1395 return Profile::staticGet('nickname', $nickname);
1397 } else if (is_numeric($id)) {
1398 return Profile::staticGet($id);
1400 $nickname = common_canonical_nickname($id);
1401 return Profile::staticGet('nickname', $nickname);
1405 function getTargetGroup($id)
1408 if (is_numeric($this->arg('id'))) {
1409 return User_group::staticGet($this->arg('id'));
1410 } else if ($this->arg('id')) {
1411 $nickname = common_canonical_nickname($this->arg('id'));
1412 $local = Local_group::staticGet('nickname', $nickname);
1413 if (empty($local)) {
1416 return User_group::staticGet('id', $local->id);
1418 } else if ($this->arg('group_id')) {
1419 // This is to ensure that a non-numeric user_id still
1420 // overrides screen_name even if it doesn't get used
1421 if (is_numeric($this->arg('group_id'))) {
1422 return User_group::staticGet('id', $this->arg('group_id'));
1424 } else if ($this->arg('group_name')) {
1425 $nickname = common_canonical_nickname($this->arg('group_name'));
1426 $local = Local_group::staticGet('nickname', $nickname);
1427 if (empty($local)) {
1430 return User_group::staticGet('id', $local->group_id);
1434 } else if (is_numeric($id)) {
1435 return User_group::staticGet($id);
1437 $nickname = common_canonical_nickname($id);
1438 $local = Local_group::staticGet('nickname', $nickname);
1439 if (empty($local)) {
1442 return User_group::staticGet('id', $local->group_id);
1448 * Returns query argument or default value if not found. Certain
1449 * parameters used throughout the API are lightly scrubbed and
1450 * bounds checked. This overrides Action::arg().
1452 * @param string $key requested argument
1453 * @param string $def default value to return if $key is not provided
1457 function arg($key, $def=null)
1460 // XXX: Do even more input validation/scrubbing?
1462 if (array_key_exists($key, $this->args)) {
1465 $page = (int)$this->args['page'];
1466 return ($page < 1) ? 1 : $page;
1468 $count = (int)$this->args['count'];
1471 } elseif ($count > 200) {
1477 $since_id = (int)$this->args['since_id'];
1478 return ($since_id < 1) ? 0 : $since_id;
1480 $max_id = (int)$this->args['max_id'];
1481 return ($max_id < 1) ? 0 : $max_id;
1483 return parent::arg($key, $def);
1491 * Calculate the complete URI that called up this action. Used for
1492 * Atom rel="self" links. Warning: this is funky.
1494 * @return string URL a URL suitable for rel="self" Atom links
1496 function getSelfUri()
1498 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1500 $id = $this->arg('id');
1501 $aargs = array('format' => $this->format);
1506 $tag = $this->arg('tag');
1508 $aargs['tag'] = $tag;
1511 parse_str($_SERVER['QUERY_STRING'], $params);
1513 if (!empty($params)) {
1514 unset($params['p']);
1515 $pstring = http_build_query($params);
1518 $uri = common_local_url($action, $aargs);
1520 if (!empty($pstring)) {
1521 $uri .= '?' . $pstring;