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/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
124 var $auth_user = null;
128 var $since_id = null;
130 var $callback = null;
132 var $access = self::READ_ONLY; // read (default) or read-write
134 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
139 * @param array $args Web and URL arguments
141 * @return boolean false if user doesn't exist
143 protected function prepare(array $args=array())
145 StatusNet::setApi(true); // reduce exception reports to aid in debugging
146 parent::prepare($args);
148 $this->format = $this->arg('format');
149 $this->callback = $this->arg('callback');
150 $this->page = (int)$this->arg('page', 1);
151 $this->count = (int)$this->arg('count', 20);
152 $this->max_id = (int)$this->arg('max_id', 0);
153 $this->since_id = (int)$this->arg('since_id', 0);
155 if ($this->arg('since')) {
156 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
159 $this->source = $this->trimmed('source');
161 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
162 $this->source = 'api';
171 * @param array $args Arguments from $_REQUEST
175 protected function handle()
177 header('Access-Control-Allow-Origin: *');
182 * Overrides XMLOutputter::element to write booleans as strings (true|false).
183 * See that method's documentation for more info.
185 * @param string $tag Element type or tagname
186 * @param array $attrs Array of element attributes, as
188 * @param string $content string content of the element
192 function element($tag, $attrs=null, $content=null)
194 if (is_bool($content)) {
195 $content = ($content ? 'true' : 'false');
198 return parent::element($tag, $attrs, $content);
201 function twitterUserArray($profile, $get_notice=false)
203 $twitter_user = array();
206 $user = $profile->getUser();
207 } catch (NoSuchUserException $e) {
211 $twitter_user['id'] = intval($profile->id);
212 $twitter_user['name'] = $profile->getBestName();
213 $twitter_user['screen_name'] = $profile->nickname;
214 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
215 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
217 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
218 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
219 // START introduced by qvitter API, not necessary for StatusNet API
220 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
222 $avatar = Avatar::getUploaded($profile);
223 $origurl = $avatar->displayUrl();
224 } catch (Exception $e) {
225 $origurl = $twitter_user['profile_image_url_profile_size'];
227 $twitter_user['profile_image_url_original'] = $origurl;
229 $twitter_user['groups_count'] = $profile->getGroups(0, null)->N;
230 foreach (array('linkcolor', 'backgroundcolor') as $key) {
231 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
233 // END introduced by qvitter API, not necessary for StatusNet API
235 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
236 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
237 $twitter_user['followers_count'] = $profile->subscriberCount();
239 // Note: some profiles don't have an associated user
241 $twitter_user['friends_count'] = $profile->subscriptionCount();
243 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
245 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
249 if (!empty($user) && $user->timezone) {
250 $timezone = $user->timezone;
254 $t->setTimezone(new DateTimeZone($timezone));
256 $twitter_user['utc_offset'] = $t->format('Z');
257 $twitter_user['time_zone'] = $timezone;
258 $twitter_user['statuses_count'] = $profile->noticeCount();
260 // Is the requesting user following this user?
261 $twitter_user['following'] = false;
262 $twitter_user['statusnet_blocking'] = false;
263 $twitter_user['notifications'] = false;
265 if (isset($this->auth_user)) {
267 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
268 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
271 $sub = Subscription::pkeyGet(array('subscriber' =>
272 $this->auth_user->id,
273 'subscribed' => $profile->id));
276 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
281 $notice = $profile->getCurrentNotice();
282 if ($notice instanceof Notice) {
284 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
288 // StatusNet-specific
290 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
292 return $twitter_user;
295 function twitterStatusArray($notice, $include_user=true)
297 $base = $this->twitterSimpleStatusArray($notice, $include_user);
299 if (!empty($notice->repeat_of)) {
300 $original = Notice::getKV('id', $notice->repeat_of);
301 if (!empty($original)) {
302 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
303 $base['retweeted_status'] = $original_array;
310 function twitterSimpleStatusArray($notice, $include_user=true)
312 $profile = $notice->getProfile();
314 $twitter_status = array();
315 $twitter_status['text'] = $notice->content;
316 $twitter_status['truncated'] = false; # Not possible on StatusNet
317 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
319 $in_reply_to = $notice->getParent()->id;
320 } catch (Exception $e) {
323 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
327 $ns = $notice->getSource();
329 if (!empty($ns->name) && !empty($ns->url)) {
330 $source = '<a href="'
331 . htmlspecialchars($ns->url)
332 . '" rel="nofollow">'
333 . htmlspecialchars($ns->name)
340 $twitter_status['uri'] = $notice->getUri();
341 $twitter_status['source'] = $source;
342 $twitter_status['id'] = intval($notice->id);
344 $replier_profile = null;
346 if ($notice->reply_to) {
347 $reply = Notice::getKV(intval($notice->reply_to));
349 $replier_profile = $reply->getProfile();
353 $twitter_status['in_reply_to_user_id'] =
354 ($replier_profile) ? intval($replier_profile->id) : null;
355 $twitter_status['in_reply_to_screen_name'] =
356 ($replier_profile) ? $replier_profile->nickname : null;
358 if (isset($notice->lat) && isset($notice->lon)) {
359 // This is the format that GeoJSON expects stuff to be in
360 $twitter_status['geo'] = array('type' => 'Point',
361 'coordinates' => array((float) $notice->lat,
362 (float) $notice->lon));
364 $twitter_status['geo'] = null;
367 if (!is_null($this->scoped)) {
368 $twitter_status['favorited'] = $this->scoped->hasFave($notice);
369 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
371 $twitter_status['favorited'] = false;
372 $twitter_status['repeated'] = false;
376 $attachments = $notice->attachments();
378 if (!empty($attachments)) {
380 $twitter_status['attachments'] = array();
382 foreach ($attachments as $attachment) {
383 $enclosure_o=$attachment->getEnclosure();
385 $enclosure = array();
386 $enclosure['url'] = $enclosure_o->url;
387 $enclosure['mimetype'] = $enclosure_o->mimetype;
388 $enclosure['size'] = $enclosure_o->size;
389 $twitter_status['attachments'][] = $enclosure;
394 if ($include_user && $profile) {
395 // Don't get notice (recursive!)
396 $twitter_user = $this->twitterUserArray($profile, false);
397 $twitter_status['user'] = $twitter_user;
400 // StatusNet-specific
402 $twitter_status['statusnet_html'] = $notice->rendered;
403 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
405 return $twitter_status;
408 function twitterGroupArray($group)
410 $twitter_group = array();
412 $twitter_group['id'] = intval($group->id);
413 $twitter_group['url'] = $group->permalink();
414 $twitter_group['nickname'] = $group->nickname;
415 $twitter_group['fullname'] = $group->fullname;
417 if (isset($this->auth_user)) {
418 $twitter_group['member'] = $this->auth_user->isMember($group);
419 $twitter_group['blocked'] = Group_block::isBlocked(
421 $this->auth_user->getProfile()
425 $twitter_group['admin_count'] = $group->getAdminCount();
426 $twitter_group['member_count'] = $group->getMemberCount();
427 $twitter_group['original_logo'] = $group->original_logo;
428 $twitter_group['homepage_logo'] = $group->homepage_logo;
429 $twitter_group['stream_logo'] = $group->stream_logo;
430 $twitter_group['mini_logo'] = $group->mini_logo;
431 $twitter_group['homepage'] = $group->homepage;
432 $twitter_group['description'] = $group->description;
433 $twitter_group['location'] = $group->location;
434 $twitter_group['created'] = $this->dateTwitter($group->created);
435 $twitter_group['modified'] = $this->dateTwitter($group->modified);
437 return $twitter_group;
440 function twitterRssGroupArray($group)
443 $entry['content']=$group->description;
444 $entry['title']=$group->nickname;
445 $entry['link']=$group->permalink();
446 $entry['published']=common_date_iso8601($group->created);
447 $entry['updated']==common_date_iso8601($group->modified);
448 $taguribase = common_config('integration', 'groupuri');
449 $entry['id'] = "group:$groupuribase:$entry[link]";
451 $entry['description'] = $entry['content'];
452 $entry['pubDate'] = common_date_rfc2822($group->created);
453 $entry['guid'] = $entry['link'];
458 function twitterListArray($list)
460 $profile = Profile::getKV('id', $list->tagger);
462 $twitter_list = array();
463 $twitter_list['id'] = $list->id;
464 $twitter_list['name'] = $list->tag;
465 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
466 $twitter_list['slug'] = $list->tag;
467 $twitter_list['description'] = $list->description;
468 $twitter_list['subscriber_count'] = $list->subscriberCount();
469 $twitter_list['member_count'] = $list->taggedCount();
470 $twitter_list['uri'] = $list->getUri();
472 if (isset($this->auth_user)) {
473 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
475 $twitter_list['following'] = false;
478 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
479 $twitter_list['user'] = $this->twitterUserArray($profile, false);
481 return $twitter_list;
484 function twitterRssEntryArray($notice)
488 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
489 $profile = $notice->getProfile();
491 // We trim() to avoid extraneous whitespace in the output
493 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
494 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
495 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
496 $entry['published'] = common_date_iso8601($notice->created);
498 $taguribase = TagURI::base();
499 $entry['id'] = "tag:$taguribase:$entry[link]";
501 $entry['updated'] = $entry['published'];
502 $entry['author'] = $profile->getBestName();
505 $attachments = $notice->attachments();
506 $enclosures = array();
508 foreach ($attachments as $attachment) {
509 $enclosure_o=$attachment->getEnclosure();
511 $enclosure = array();
512 $enclosure['url'] = $enclosure_o->url;
513 $enclosure['mimetype'] = $enclosure_o->mimetype;
514 $enclosure['size'] = $enclosure_o->size;
515 $enclosures[] = $enclosure;
519 if (!empty($enclosures)) {
520 $entry['enclosures'] = $enclosures;
524 $tag = new Notice_tag();
525 $tag->notice_id = $notice->id;
527 $entry['tags']=array();
528 while ($tag->fetch()) {
529 $entry['tags'][]=$tag->tag;
535 $entry['description'] = $entry['content'];
536 $entry['pubDate'] = common_date_rfc2822($notice->created);
537 $entry['guid'] = $entry['link'];
539 if (isset($notice->lat) && isset($notice->lon)) {
540 // This is the format that GeoJSON expects stuff to be in.
541 // showGeoRSS() below uses it for XML output, so we reuse it
542 $entry['geo'] = array('type' => 'Point',
543 'coordinates' => array((float) $notice->lat,
544 (float) $notice->lon));
546 $entry['geo'] = null;
549 Event::handle('EndRssEntryArray', array($notice, &$entry));
555 function twitterRelationshipArray($source, $target)
557 $relationship = array();
559 $relationship['source'] =
560 $this->relationshipDetailsArray($source, $target);
561 $relationship['target'] =
562 $this->relationshipDetailsArray($target, $source);
564 return array('relationship' => $relationship);
567 function relationshipDetailsArray($source, $target)
571 $details['screen_name'] = $source->nickname;
572 $details['followed_by'] = $target->isSubscribed($source);
573 $details['following'] = $source->isSubscribed($target);
575 $notifications = false;
577 if ($source->isSubscribed($target)) {
578 $sub = Subscription::pkeyGet(array('subscriber' =>
579 $source->id, 'subscribed' => $target->id));
582 $notifications = ($sub->jabber || $sub->sms);
586 $details['notifications_enabled'] = $notifications;
587 $details['blocking'] = $source->hasBlocked($target);
588 $details['id'] = intval($source->id);
593 function showTwitterXmlRelationship($relationship)
595 $this->elementStart('relationship');
597 foreach($relationship as $element => $value) {
598 if ($element == 'source' || $element == 'target') {
599 $this->elementStart($element);
600 $this->showXmlRelationshipDetails($value);
601 $this->elementEnd($element);
605 $this->elementEnd('relationship');
608 function showXmlRelationshipDetails($details)
610 foreach($details as $element => $value) {
611 $this->element($element, null, $value);
615 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
619 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
621 $this->elementStart($tag, $attrs);
622 foreach($twitter_status as $element => $value) {
625 $this->showTwitterXmlUser($twitter_status['user']);
628 $this->element($element, null, common_xml_safe_str($value));
631 $this->showXmlAttachments($twitter_status['attachments']);
634 $this->showGeoXML($value);
636 case 'retweeted_status':
637 $this->showTwitterXmlStatus($value, 'retweeted_status');
640 if (strncmp($element, 'statusnet_', 10) == 0) {
641 $this->element('statusnet:'.substr($element, 10), null, $value);
643 $this->element($element, null, $value);
647 $this->elementEnd($tag);
650 function showTwitterXmlGroup($twitter_group)
652 $this->elementStart('group');
653 foreach($twitter_group as $element => $value) {
654 $this->element($element, null, $value);
656 $this->elementEnd('group');
659 function showTwitterXmlList($twitter_list)
661 $this->elementStart('list');
662 foreach($twitter_list as $element => $value) {
663 if($element == 'user') {
664 $this->showTwitterXmlUser($value, 'user');
667 $this->element($element, null, $value);
670 $this->elementEnd('list');
673 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
677 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
679 $this->elementStart($role, $attrs);
680 foreach($twitter_user as $element => $value) {
681 if ($element == 'status') {
682 $this->showTwitterXmlStatus($twitter_user['status']);
683 } else if (strncmp($element, 'statusnet_', 10) == 0) {
684 $this->element('statusnet:'.substr($element, 10), null, $value);
686 $this->element($element, null, $value);
689 $this->elementEnd($role);
692 function showXmlAttachments($attachments) {
693 if (!empty($attachments)) {
694 $this->elementStart('attachments', array('type' => 'array'));
695 foreach ($attachments as $attachment) {
697 $attrs['url'] = $attachment['url'];
698 $attrs['mimetype'] = $attachment['mimetype'];
699 $attrs['size'] = $attachment['size'];
700 $this->element('enclosure', $attrs, '');
702 $this->elementEnd('attachments');
706 function showGeoXML($geo)
710 $this->element('geo');
712 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
713 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
714 $this->elementEnd('geo');
718 function showGeoRSS($geo)
724 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
729 function showTwitterRssItem($entry)
731 $this->elementStart('item');
732 $this->element('title', null, $entry['title']);
733 $this->element('description', null, $entry['description']);
734 $this->element('pubDate', null, $entry['pubDate']);
735 $this->element('guid', null, $entry['guid']);
736 $this->element('link', null, $entry['link']);
738 // RSS only supports 1 enclosure per item
739 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
740 $enclosure = $entry['enclosures'][0];
741 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
744 if(array_key_exists('tags', $entry)){
745 foreach($entry['tags'] as $tag){
746 $this->element('category', null,$tag);
750 $this->showGeoRSS($entry['geo']);
751 $this->elementEnd('item');
754 function showJsonObjects($objects)
756 print(json_encode($objects));
759 function showSingleXmlStatus($notice)
761 $this->initDocument('xml');
762 $twitter_status = $this->twitterStatusArray($notice);
763 $this->showTwitterXmlStatus($twitter_status, 'status', true);
764 $this->endDocument('xml');
767 function showSingleAtomStatus($notice)
769 header('Content-Type: application/atom+xml; charset=utf-8');
770 print $notice->asAtomEntry(true, true, true, $this->auth_user);
773 function show_single_json_status($notice)
775 $this->initDocument('json');
776 $status = $this->twitterStatusArray($notice);
777 $this->showJsonObjects($status);
778 $this->endDocument('json');
781 function showXmlTimeline($notice)
783 $this->initDocument('xml');
784 $this->elementStart('statuses', array('type' => 'array',
785 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
787 if (is_array($notice)) {
788 $notice = new ArrayWrapper($notice);
791 while ($notice->fetch()) {
793 $twitter_status = $this->twitterStatusArray($notice);
794 $this->showTwitterXmlStatus($twitter_status);
795 } catch (Exception $e) {
796 common_log(LOG_ERR, $e->getMessage());
801 $this->elementEnd('statuses');
802 $this->endDocument('xml');
805 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
807 $this->initDocument('rss');
809 $this->element('title', null, $title);
810 $this->element('link', null, $link);
812 if (!is_null($self)) {
816 'type' => 'application/rss+xml',
823 if (!is_null($suplink)) {
824 // For FriendFeed's SUP protocol
825 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
826 'rel' => 'http://api.friendfeed.com/2008/03#sup',
828 'type' => 'application/json'));
831 if (!is_null($logo)) {
832 $this->elementStart('image');
833 $this->element('link', null, $link);
834 $this->element('title', null, $title);
835 $this->element('url', null, $logo);
836 $this->elementEnd('image');
839 $this->element('description', null, $subtitle);
840 $this->element('language', null, 'en-us');
841 $this->element('ttl', null, '40');
843 if (is_array($notice)) {
844 $notice = new ArrayWrapper($notice);
847 while ($notice->fetch()) {
849 $entry = $this->twitterRssEntryArray($notice);
850 $this->showTwitterRssItem($entry);
851 } catch (Exception $e) {
852 common_log(LOG_ERR, $e->getMessage());
853 // continue on exceptions
857 $this->endTwitterRss();
860 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
862 $this->initDocument('atom');
864 $this->element('title', null, $title);
865 $this->element('id', null, $id);
866 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
868 if (!is_null($logo)) {
869 $this->element('logo',null,$logo);
872 if (!is_null($suplink)) {
873 // For FriendFeed's SUP protocol
874 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
876 'type' => 'application/json'));
879 if (!is_null($selfuri)) {
880 $this->element('link', array('href' => $selfuri,
881 'rel' => 'self', 'type' => 'application/atom+xml'), null);
884 $this->element('updated', null, common_date_iso8601('now'));
885 $this->element('subtitle', null, $subtitle);
887 if (is_array($notice)) {
888 $notice = new ArrayWrapper($notice);
891 while ($notice->fetch()) {
893 $this->raw($notice->asAtomEntry());
894 } catch (Exception $e) {
895 common_log(LOG_ERR, $e->getMessage());
900 $this->endDocument('atom');
903 function showRssGroups($group, $title, $link, $subtitle)
905 $this->initDocument('rss');
907 $this->element('title', null, $title);
908 $this->element('link', null, $link);
909 $this->element('description', null, $subtitle);
910 $this->element('language', null, 'en-us');
911 $this->element('ttl', null, '40');
913 if (is_array($group)) {
914 foreach ($group as $g) {
915 $twitter_group = $this->twitterRssGroupArray($g);
916 $this->showTwitterRssItem($twitter_group);
919 while ($group->fetch()) {
920 $twitter_group = $this->twitterRssGroupArray($group);
921 $this->showTwitterRssItem($twitter_group);
925 $this->endTwitterRss();
928 function showTwitterAtomEntry($entry)
930 $this->elementStart('entry');
931 $this->element('title', null, common_xml_safe_str($entry['title']));
934 array('type' => 'html'),
935 common_xml_safe_str($entry['content'])
937 $this->element('id', null, $entry['id']);
938 $this->element('published', null, $entry['published']);
939 $this->element('updated', null, $entry['updated']);
940 $this->element('link', array('type' => 'text/html',
941 'href' => $entry['link'],
942 'rel' => 'alternate'));
943 $this->element('link', array('type' => $entry['avatar-type'],
944 'href' => $entry['avatar'],
946 $this->elementStart('author');
948 $this->element('name', null, $entry['author-name']);
949 $this->element('uri', null, $entry['author-uri']);
951 $this->elementEnd('author');
952 $this->elementEnd('entry');
955 function showXmlDirectMessage($dm, $namespaces=false)
959 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
961 $this->elementStart('direct_message', $attrs);
962 foreach($dm as $element => $value) {
966 $this->showTwitterXmlUser($value, $element);
969 $this->element($element, null, common_xml_safe_str($value));
972 $this->element($element, null, $value);
976 $this->elementEnd('direct_message');
979 function directMessageArray($message)
983 $from_profile = $message->getFrom();
984 $to_profile = $message->getTo();
986 $dmsg['id'] = intval($message->id);
987 $dmsg['sender_id'] = intval($from_profile->id);
988 $dmsg['text'] = trim($message->content);
989 $dmsg['recipient_id'] = intval($to_profile->id);
990 $dmsg['created_at'] = $this->dateTwitter($message->created);
991 $dmsg['sender_screen_name'] = $from_profile->nickname;
992 $dmsg['recipient_screen_name'] = $to_profile->nickname;
993 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
994 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
999 function rssDirectMessageArray($message)
1003 $from = $message->getFrom();
1005 $entry['title'] = sprintf('Message from %1$s to %2$s',
1006 $from->nickname, $message->getTo()->nickname);
1008 $entry['content'] = common_xml_safe_str($message->rendered);
1009 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1010 $entry['published'] = common_date_iso8601($message->created);
1012 $taguribase = TagURI::base();
1014 $entry['id'] = "tag:$taguribase:$entry[link]";
1015 $entry['updated'] = $entry['published'];
1017 $entry['author-name'] = $from->getBestName();
1018 $entry['author-uri'] = $from->homepage;
1020 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1022 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1023 $entry['avatar-type'] = $avatar->mediatype;
1024 } catch (Exception $e) {
1025 $entry['avatar-type'] = 'image/png';
1028 // RSS item specific
1030 $entry['description'] = $entry['content'];
1031 $entry['pubDate'] = common_date_rfc2822($message->created);
1032 $entry['guid'] = $entry['link'];
1037 function showSingleXmlDirectMessage($message)
1039 $this->initDocument('xml');
1040 $dmsg = $this->directMessageArray($message);
1041 $this->showXmlDirectMessage($dmsg, true);
1042 $this->endDocument('xml');
1045 function showSingleJsonDirectMessage($message)
1047 $this->initDocument('json');
1048 $dmsg = $this->directMessageArray($message);
1049 $this->showJsonObjects($dmsg);
1050 $this->endDocument('json');
1053 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1055 $this->initDocument('atom');
1057 $this->element('title', null, common_xml_safe_str($title));
1058 $this->element('id', null, $id);
1059 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1061 if (!is_null($selfuri)) {
1062 $this->element('link', array('href' => $selfuri,
1063 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1066 $this->element('updated', null, common_date_iso8601('now'));
1067 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1069 if (is_array($group)) {
1070 foreach ($group as $g) {
1071 $this->raw($g->asAtomEntry());
1074 while ($group->fetch()) {
1075 $this->raw($group->asAtomEntry());
1079 $this->endDocument('atom');
1083 function showJsonTimeline($notice)
1085 $this->initDocument('json');
1087 $statuses = array();
1089 if (is_array($notice)) {
1090 $notice = new ArrayWrapper($notice);
1093 while ($notice->fetch()) {
1095 $twitter_status = $this->twitterStatusArray($notice);
1096 array_push($statuses, $twitter_status);
1097 } catch (Exception $e) {
1098 common_log(LOG_ERR, $e->getMessage());
1103 $this->showJsonObjects($statuses);
1105 $this->endDocument('json');
1108 function showJsonGroups($group)
1110 $this->initDocument('json');
1114 if (is_array($group)) {
1115 foreach ($group as $g) {
1116 $twitter_group = $this->twitterGroupArray($g);
1117 array_push($groups, $twitter_group);
1120 while ($group->fetch()) {
1121 $twitter_group = $this->twitterGroupArray($group);
1122 array_push($groups, $twitter_group);
1126 $this->showJsonObjects($groups);
1128 $this->endDocument('json');
1131 function showXmlGroups($group)
1134 $this->initDocument('xml');
1135 $this->elementStart('groups', array('type' => 'array'));
1137 if (is_array($group)) {
1138 foreach ($group as $g) {
1139 $twitter_group = $this->twitterGroupArray($g);
1140 $this->showTwitterXmlGroup($twitter_group);
1143 while ($group->fetch()) {
1144 $twitter_group = $this->twitterGroupArray($group);
1145 $this->showTwitterXmlGroup($twitter_group);
1149 $this->elementEnd('groups');
1150 $this->endDocument('xml');
1153 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1156 $this->initDocument('xml');
1157 $this->elementStart('lists_list');
1158 $this->elementStart('lists', array('type' => 'array'));
1160 if (is_array($list)) {
1161 foreach ($list as $l) {
1162 $twitter_list = $this->twitterListArray($l);
1163 $this->showTwitterXmlList($twitter_list);
1166 while ($list->fetch()) {
1167 $twitter_list = $this->twitterListArray($list);
1168 $this->showTwitterXmlList($twitter_list);
1172 $this->elementEnd('lists');
1174 $this->element('next_cursor', null, $next_cursor);
1175 $this->element('previous_cursor', null, $prev_cursor);
1177 $this->elementEnd('lists_list');
1178 $this->endDocument('xml');
1181 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1183 $this->initDocument('json');
1187 if (is_array($list)) {
1188 foreach ($list as $l) {
1189 $twitter_list = $this->twitterListArray($l);
1190 array_push($lists, $twitter_list);
1193 while ($list->fetch()) {
1194 $twitter_list = $this->twitterListArray($list);
1195 array_push($lists, $twitter_list);
1199 $lists_list = array(
1201 'next_cursor' => $next_cursor,
1202 'next_cursor_str' => strval($next_cursor),
1203 'previous_cursor' => $prev_cursor,
1204 'previous_cursor_str' => strval($prev_cursor)
1207 $this->showJsonObjects($lists_list);
1209 $this->endDocument('json');
1212 function showTwitterXmlUsers($user)
1214 $this->initDocument('xml');
1215 $this->elementStart('users', array('type' => 'array',
1216 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1218 if (is_array($user)) {
1219 foreach ($user as $u) {
1220 $twitter_user = $this->twitterUserArray($u);
1221 $this->showTwitterXmlUser($twitter_user);
1224 while ($user->fetch()) {
1225 $twitter_user = $this->twitterUserArray($user);
1226 $this->showTwitterXmlUser($twitter_user);
1230 $this->elementEnd('users');
1231 $this->endDocument('xml');
1234 function showJsonUsers($user)
1236 $this->initDocument('json');
1240 if (is_array($user)) {
1241 foreach ($user as $u) {
1242 $twitter_user = $this->twitterUserArray($u);
1243 array_push($users, $twitter_user);
1246 while ($user->fetch()) {
1247 $twitter_user = $this->twitterUserArray($user);
1248 array_push($users, $twitter_user);
1252 $this->showJsonObjects($users);
1254 $this->endDocument('json');
1257 function showSingleJsonGroup($group)
1259 $this->initDocument('json');
1260 $twitter_group = $this->twitterGroupArray($group);
1261 $this->showJsonObjects($twitter_group);
1262 $this->endDocument('json');
1265 function showSingleXmlGroup($group)
1267 $this->initDocument('xml');
1268 $twitter_group = $this->twitterGroupArray($group);
1269 $this->showTwitterXmlGroup($twitter_group);
1270 $this->endDocument('xml');
1273 function showSingleJsonList($list)
1275 $this->initDocument('json');
1276 $twitter_list = $this->twitterListArray($list);
1277 $this->showJsonObjects($twitter_list);
1278 $this->endDocument('json');
1281 function showSingleXmlList($list)
1283 $this->initDocument('xml');
1284 $twitter_list = $this->twitterListArray($list);
1285 $this->showTwitterXmlList($twitter_list);
1286 $this->endDocument('xml');
1289 function dateTwitter($dt)
1291 $dateStr = date('d F Y H:i:s', strtotime($dt));
1292 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1293 $d->setTimezone(new DateTimeZone(common_timezone()));
1294 return $d->format('D M d H:i:s O Y');
1297 function initDocument($type='xml')
1301 header('Content-Type: application/xml; charset=utf-8');
1305 header('Content-Type: application/json; charset=utf-8');
1307 // Check for JSONP callback
1308 if (isset($this->callback)) {
1309 print $this->callback . '(';
1313 header("Content-Type: application/rss+xml; charset=utf-8");
1314 $this->initTwitterRss();
1317 header('Content-Type: application/atom+xml; charset=utf-8');
1318 $this->initTwitterAtom();
1321 // TRANS: Client error on an API request with an unsupported data format.
1322 $this->clientError(_('Not a supported data format.'));
1329 function endDocument($type='xml')
1336 // Check for JSONP callback
1337 if (isset($this->callback)) {
1342 $this->endTwitterRss();
1345 $this->endTwitterRss();
1348 // TRANS: Client error on an API request with an unsupported data format.
1349 $this->clientError(_('Not a supported data format.'));
1355 function clientError($msg, $code = 400, $format = null)
1357 $action = $this->trimmed('action');
1358 if ($format === null) {
1359 $format = $this->format;
1362 common_debug("User error '$code' on '$action': $msg", __FILE__);
1364 if (!array_key_exists($code, ClientErrorAction::$status)) {
1368 $status_string = ClientErrorAction::$status[$code];
1370 // Do not emit error header for JSONP
1371 if (!isset($this->callback)) {
1372 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1377 $this->initDocument('xml');
1378 $this->elementStart('hash');
1379 $this->element('error', null, $msg);
1380 $this->element('request', null, $_SERVER['REQUEST_URI']);
1381 $this->elementEnd('hash');
1382 $this->endDocument('xml');
1385 $this->initDocument('json');
1386 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1387 print(json_encode($error_array));
1388 $this->endDocument('json');
1391 header('Content-Type: text/plain; charset=utf-8');
1395 // If user didn't request a useful format, throw a regular client error
1396 throw new ClientException($msg, $code);
1400 function serverError($msg, $code = 500, $content_type = null)
1402 $action = $this->trimmed('action');
1403 if ($content_type === null) {
1404 $content_type = $this->format;
1407 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1409 if (!array_key_exists($code, ServerErrorAction::$status)) {
1413 $status_string = ServerErrorAction::$status[$code];
1415 // Do not emit error header for JSONP
1416 if (!isset($this->callback)) {
1417 header('HTTP/1.1 '.$code.' '.$status_string);
1420 if ($content_type == 'xml') {
1421 $this->initDocument('xml');
1422 $this->elementStart('hash');
1423 $this->element('error', null, $msg);
1424 $this->element('request', null, $_SERVER['REQUEST_URI']);
1425 $this->elementEnd('hash');
1426 $this->endDocument('xml');
1428 $this->initDocument('json');
1429 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1430 print(json_encode($error_array));
1431 $this->endDocument('json');
1435 function initTwitterRss()
1438 $this->elementStart(
1442 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1443 'xmlns:georss' => 'http://www.georss.org/georss'
1446 $this->elementStart('channel');
1447 Event::handle('StartApiRss', array($this));
1450 function endTwitterRss()
1452 $this->elementEnd('channel');
1453 $this->elementEnd('rss');
1457 function initTwitterAtom()
1460 // FIXME: don't hardcode the language here!
1461 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1462 'xml:lang' => 'en-US',
1463 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1466 function endTwitterAtom()
1468 $this->elementEnd('feed');
1472 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1474 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1475 switch ($content_type) {
1477 $this->showTwitterXmlUser($profile_array);
1480 $this->showJsonObjects($profile_array);
1483 // TRANS: Client error on an API request with an unsupported data format.
1484 $this->clientError(_('Not a supported data format.'));
1490 private static function is_decimal($str)
1492 return preg_match('/^[0-9]+$/', $str);
1495 function getTargetUser($id)
1498 // Twitter supports these other ways of passing the user ID
1499 if (self::is_decimal($this->arg('id'))) {
1500 return User::getKV($this->arg('id'));
1501 } else if ($this->arg('id')) {
1502 $nickname = common_canonical_nickname($this->arg('id'));
1503 return User::getKV('nickname', $nickname);
1504 } else if ($this->arg('user_id')) {
1505 // This is to ensure that a non-numeric user_id still
1506 // overrides screen_name even if it doesn't get used
1507 if (self::is_decimal($this->arg('user_id'))) {
1508 return User::getKV('id', $this->arg('user_id'));
1510 } else if ($this->arg('screen_name')) {
1511 $nickname = common_canonical_nickname($this->arg('screen_name'));
1512 return User::getKV('nickname', $nickname);
1514 // Fall back to trying the currently authenticated user
1515 return $this->auth_user;
1518 } else if (self::is_decimal($id)) {
1519 return User::getKV($id);
1521 $nickname = common_canonical_nickname($id);
1522 return User::getKV('nickname', $nickname);
1526 function getTargetProfile($id)
1530 // Twitter supports these other ways of passing the user ID
1531 if (self::is_decimal($this->arg('id'))) {
1532 return Profile::getKV($this->arg('id'));
1533 } else if ($this->arg('id')) {
1534 // Screen names currently can only uniquely identify a local user.
1535 $nickname = common_canonical_nickname($this->arg('id'));
1536 $user = User::getKV('nickname', $nickname);
1537 return $user ? $user->getProfile() : null;
1538 } else if ($this->arg('user_id')) {
1539 // This is to ensure that a non-numeric user_id still
1540 // overrides screen_name even if it doesn't get used
1541 if (self::is_decimal($this->arg('user_id'))) {
1542 return Profile::getKV('id', $this->arg('user_id'));
1544 } else if ($this->arg('screen_name')) {
1545 $nickname = common_canonical_nickname($this->arg('screen_name'));
1546 $user = User::getKV('nickname', $nickname);
1547 return $user ? $user->getProfile() : null;
1549 } else if (self::is_decimal($id)) {
1550 return Profile::getKV($id);
1552 $nickname = common_canonical_nickname($id);
1553 $user = User::getKV('nickname', $nickname);
1554 return $user ? $user->getProfile() : null;
1558 function getTargetGroup($id)
1561 if (self::is_decimal($this->arg('id'))) {
1562 return User_group::getKV('id', $this->arg('id'));
1563 } else if ($this->arg('id')) {
1564 return User_group::getForNickname($this->arg('id'));
1565 } else if ($this->arg('group_id')) {
1566 // This is to ensure that a non-numeric group_id still
1567 // overrides group_name even if it doesn't get used
1568 if (self::is_decimal($this->arg('group_id'))) {
1569 return User_group::getKV('id', $this->arg('group_id'));
1571 } else if ($this->arg('group_name')) {
1572 return User_group::getForNickname($this->arg('group_name'));
1575 } else if (self::is_decimal($id)) {
1576 return User_group::getKV('id', $id);
1577 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1578 return User_group::getKV('uri', urldecode($this->arg('uri')));
1580 return User_group::getForNickname($id);
1584 function getTargetList($user=null, $id=null)
1586 $tagger = $this->getTargetUser($user);
1590 $id = $this->arg('id');
1594 if (is_numeric($id)) {
1595 $list = Profile_list::getKV('id', $id);
1597 // only if the list with the id belongs to the tagger
1598 if(empty($list) || $list->tagger != $tagger->id) {
1603 $tag = common_canonical_tag($id);
1604 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1607 if (!empty($list) && $list->private) {
1608 if ($this->auth_user->id == $list->tagger) {
1619 * Returns query argument or default value if not found. Certain
1620 * parameters used throughout the API are lightly scrubbed and
1621 * bounds checked. This overrides Action::arg().
1623 * @param string $key requested argument
1624 * @param string $def default value to return if $key is not provided
1628 function arg($key, $def=null)
1630 // XXX: Do even more input validation/scrubbing?
1632 if (array_key_exists($key, $this->args)) {
1635 $page = (int)$this->args['page'];
1636 return ($page < 1) ? 1 : $page;
1638 $count = (int)$this->args['count'];
1641 } elseif ($count > 200) {
1647 $since_id = (int)$this->args['since_id'];
1648 return ($since_id < 1) ? 0 : $since_id;
1650 $max_id = (int)$this->args['max_id'];
1651 return ($max_id < 1) ? 0 : $max_id;
1653 return parent::arg($key, $def);
1661 * Calculate the complete URI that called up this action. Used for
1662 * Atom rel="self" links. Warning: this is funky.
1664 * @return string URL a URL suitable for rel="self" Atom links
1666 function getSelfUri()
1668 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1670 $id = $this->arg('id');
1671 $aargs = array('format' => $this->format);
1676 $tag = $this->arg('tag');
1678 $aargs['tag'] = $tag;
1681 parse_str($_SERVER['QUERY_STRING'], $params);
1683 if (!empty($params)) {
1684 unset($params['p']);
1685 $pstring = http_build_query($params);
1688 $uri = common_local_url($action, $aargs);
1690 if (!empty($pstring)) {
1691 $uri .= '?' . $pstring;