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;
123 var $auth_user = null;
127 var $since_id = null;
129 var $callback = null;
131 var $access = self::READ_ONLY; // read (default) or read-write
133 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
138 * @param array $args Web and URL arguments
140 * @return boolean false if user doesn't exist
142 protected function prepare(array $args=array())
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
148 $this->callback = $this->arg('callback');
149 $this->page = (int)$this->arg('page', 1);
150 $this->count = (int)$this->arg('count', 20);
151 $this->max_id = (int)$this->arg('max_id', 0);
152 $this->since_id = (int)$this->arg('since_id', 0);
154 if ($this->arg('since')) {
155 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
158 $this->source = $this->trimmed('source');
160 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
161 $this->source = 'api';
170 * @param array $args Arguments from $_REQUEST
174 protected function handle()
176 header('Access-Control-Allow-Origin: *');
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();
205 $user = $profile->getUser();
206 } catch (NoSuchUserException $e) {
210 $twitter_user['id'] = intval($profile->id);
211 $twitter_user['name'] = $profile->getBestName();
212 $twitter_user['screen_name'] = $profile->nickname;
213 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
214 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
216 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
217 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
218 // START introduced by qvitter API, not necessary for StatusNet API
219 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
221 $avatar = Avatar::getUploaded($profile);
222 $origurl = $avatar->displayUrl();
223 } catch (Exception $e) {
224 $origurl = $twitter_user['profile_image_url_profile_size'];
226 $twitter_user['profile_image_url_original'] = $origurl;
228 $twitter_user['groups_count'] = $profile->getGroupCount();
229 foreach (array('linkcolor', 'backgroundcolor') as $key) {
230 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
232 // END introduced by qvitter API, not necessary for StatusNet API
234 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
235 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
236 $twitter_user['followers_count'] = $profile->subscriberCount();
238 // Note: some profiles don't have an associated user
240 $twitter_user['friends_count'] = $profile->subscriptionCount();
242 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
244 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
248 if (!empty($user) && $user->timezone) {
249 $timezone = $user->timezone;
253 $t->setTimezone(new DateTimeZone($timezone));
255 $twitter_user['utc_offset'] = $t->format('Z');
256 $twitter_user['time_zone'] = $timezone;
257 $twitter_user['statuses_count'] = $profile->noticeCount();
259 // Is the requesting user following this user?
260 $twitter_user['following'] = false;
261 $twitter_user['statusnet_blocking'] = false;
262 $twitter_user['notifications'] = false;
264 if (isset($this->auth_user)) {
266 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
267 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
270 $sub = Subscription::pkeyGet(array('subscriber' =>
271 $this->auth_user->id,
272 'subscribed' => $profile->id));
275 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
280 $notice = $profile->getCurrentNotice();
281 if ($notice instanceof Notice) {
283 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
287 // StatusNet-specific
289 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
291 return $twitter_user;
294 function twitterStatusArray($notice, $include_user=true)
296 $base = $this->twitterSimpleStatusArray($notice, $include_user);
298 if (!empty($notice->repeat_of)) {
299 $original = Notice::getKV('id', $notice->repeat_of);
300 if (!empty($original)) {
301 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
302 $base['retweeted_status'] = $original_array;
309 function twitterSimpleStatusArray($notice, $include_user=true)
311 $profile = $notice->getProfile();
313 $twitter_status = array();
314 $twitter_status['text'] = $notice->content;
315 $twitter_status['truncated'] = false; # Not possible on StatusNet
316 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
318 // We could just do $notice->reply_to but maybe the future holds a
319 // different story for parenting.
320 $parent = $notice->getParent();
321 $in_reply_to = $parent->id;
322 } catch (Exception $e) {
325 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
329 $ns = $notice->getSource();
331 if (!empty($ns->name) && !empty($ns->url)) {
332 $source = '<a href="'
333 . htmlspecialchars($ns->url)
334 . '" rel="nofollow">'
335 . htmlspecialchars($ns->name)
342 $twitter_status['uri'] = $notice->getUri();
343 $twitter_status['source'] = $source;
344 $twitter_status['id'] = intval($notice->id);
346 $replier_profile = null;
348 if ($notice->reply_to) {
349 $reply = Notice::getKV(intval($notice->reply_to));
351 $replier_profile = $reply->getProfile();
355 $twitter_status['in_reply_to_user_id'] =
356 ($replier_profile) ? intval($replier_profile->id) : null;
357 $twitter_status['in_reply_to_screen_name'] =
358 ($replier_profile) ? $replier_profile->nickname : null;
360 if (isset($notice->lat) && isset($notice->lon)) {
361 // This is the format that GeoJSON expects stuff to be in
362 $twitter_status['geo'] = array('type' => 'Point',
363 'coordinates' => array((float) $notice->lat,
364 (float) $notice->lon));
366 $twitter_status['geo'] = null;
369 if (!is_null($this->scoped)) {
370 $twitter_status['favorited'] = $this->scoped->hasFave($notice);
371 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
373 $twitter_status['favorited'] = false;
374 $twitter_status['repeated'] = false;
378 $attachments = $notice->attachments();
380 if (!empty($attachments)) {
382 $twitter_status['attachments'] = array();
384 foreach ($attachments as $attachment) {
385 $enclosure_o=$attachment->getEnclosure();
387 $enclosure = array();
388 $enclosure['url'] = $enclosure_o->url;
389 $enclosure['mimetype'] = $enclosure_o->mimetype;
390 $enclosure['size'] = $enclosure_o->size;
391 $twitter_status['attachments'][] = $enclosure;
396 if ($include_user && $profile) {
397 // Don't get notice (recursive!)
398 $twitter_user = $this->twitterUserArray($profile, false);
399 $twitter_status['user'] = $twitter_user;
402 // StatusNet-specific
404 $twitter_status['statusnet_html'] = $notice->rendered;
405 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
407 return $twitter_status;
410 function twitterGroupArray($group)
412 $twitter_group = array();
414 $twitter_group['id'] = intval($group->id);
415 $twitter_group['url'] = $group->permalink();
416 $twitter_group['nickname'] = $group->nickname;
417 $twitter_group['fullname'] = $group->fullname;
419 if (isset($this->auth_user)) {
420 $twitter_group['member'] = $this->auth_user->isMember($group);
421 $twitter_group['blocked'] = Group_block::isBlocked(
423 $this->auth_user->getProfile()
427 $twitter_group['admin_count'] = $group->getAdminCount();
428 $twitter_group['member_count'] = $group->getMemberCount();
429 $twitter_group['original_logo'] = $group->original_logo;
430 $twitter_group['homepage_logo'] = $group->homepage_logo;
431 $twitter_group['stream_logo'] = $group->stream_logo;
432 $twitter_group['mini_logo'] = $group->mini_logo;
433 $twitter_group['homepage'] = $group->homepage;
434 $twitter_group['description'] = $group->description;
435 $twitter_group['location'] = $group->location;
436 $twitter_group['created'] = $this->dateTwitter($group->created);
437 $twitter_group['modified'] = $this->dateTwitter($group->modified);
439 return $twitter_group;
442 function twitterRssGroupArray($group)
445 $entry['content']=$group->description;
446 $entry['title']=$group->nickname;
447 $entry['link']=$group->permalink();
448 $entry['published']=common_date_iso8601($group->created);
449 $entry['updated']==common_date_iso8601($group->modified);
450 $taguribase = common_config('integration', 'groupuri');
451 $entry['id'] = "group:$groupuribase:$entry[link]";
453 $entry['description'] = $entry['content'];
454 $entry['pubDate'] = common_date_rfc2822($group->created);
455 $entry['guid'] = $entry['link'];
460 function twitterListArray($list)
462 $profile = Profile::getKV('id', $list->tagger);
464 $twitter_list = array();
465 $twitter_list['id'] = $list->id;
466 $twitter_list['name'] = $list->tag;
467 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
468 $twitter_list['slug'] = $list->tag;
469 $twitter_list['description'] = $list->description;
470 $twitter_list['subscriber_count'] = $list->subscriberCount();
471 $twitter_list['member_count'] = $list->taggedCount();
472 $twitter_list['uri'] = $list->getUri();
474 if (isset($this->auth_user)) {
475 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
477 $twitter_list['following'] = false;
480 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
481 $twitter_list['user'] = $this->twitterUserArray($profile, false);
483 return $twitter_list;
486 function twitterRssEntryArray($notice)
490 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
491 $profile = $notice->getProfile();
493 // We trim() to avoid extraneous whitespace in the output
495 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
496 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
497 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
498 $entry['published'] = common_date_iso8601($notice->created);
500 $taguribase = TagURI::base();
501 $entry['id'] = "tag:$taguribase:$entry[link]";
503 $entry['updated'] = $entry['published'];
504 $entry['author'] = $profile->getBestName();
507 $attachments = $notice->attachments();
508 $enclosures = array();
510 foreach ($attachments as $attachment) {
511 $enclosure_o=$attachment->getEnclosure();
513 $enclosure = array();
514 $enclosure['url'] = $enclosure_o->url;
515 $enclosure['mimetype'] = $enclosure_o->mimetype;
516 $enclosure['size'] = $enclosure_o->size;
517 $enclosures[] = $enclosure;
521 if (!empty($enclosures)) {
522 $entry['enclosures'] = $enclosures;
526 $tag = new Notice_tag();
527 $tag->notice_id = $notice->id;
529 $entry['tags']=array();
530 while ($tag->fetch()) {
531 $entry['tags'][]=$tag->tag;
537 $entry['description'] = $entry['content'];
538 $entry['pubDate'] = common_date_rfc2822($notice->created);
539 $entry['guid'] = $entry['link'];
541 if (isset($notice->lat) && isset($notice->lon)) {
542 // This is the format that GeoJSON expects stuff to be in.
543 // showGeoRSS() below uses it for XML output, so we reuse it
544 $entry['geo'] = array('type' => 'Point',
545 'coordinates' => array((float) $notice->lat,
546 (float) $notice->lon));
548 $entry['geo'] = null;
551 Event::handle('EndRssEntryArray', array($notice, &$entry));
557 function twitterRelationshipArray($source, $target)
559 $relationship = array();
561 $relationship['source'] =
562 $this->relationshipDetailsArray($source, $target);
563 $relationship['target'] =
564 $this->relationshipDetailsArray($target, $source);
566 return array('relationship' => $relationship);
569 function relationshipDetailsArray($source, $target)
573 $details['screen_name'] = $source->nickname;
574 $details['followed_by'] = $target->isSubscribed($source);
575 $details['following'] = $source->isSubscribed($target);
577 $notifications = false;
579 if ($source->isSubscribed($target)) {
580 $sub = Subscription::pkeyGet(array('subscriber' =>
581 $source->id, 'subscribed' => $target->id));
584 $notifications = ($sub->jabber || $sub->sms);
588 $details['notifications_enabled'] = $notifications;
589 $details['blocking'] = $source->hasBlocked($target);
590 $details['id'] = intval($source->id);
595 function showTwitterXmlRelationship($relationship)
597 $this->elementStart('relationship');
599 foreach($relationship as $element => $value) {
600 if ($element == 'source' || $element == 'target') {
601 $this->elementStart($element);
602 $this->showXmlRelationshipDetails($value);
603 $this->elementEnd($element);
607 $this->elementEnd('relationship');
610 function showXmlRelationshipDetails($details)
612 foreach($details as $element => $value) {
613 $this->element($element, null, $value);
617 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
621 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
623 $this->elementStart($tag, $attrs);
624 foreach($twitter_status as $element => $value) {
627 $this->showTwitterXmlUser($twitter_status['user']);
630 $this->element($element, null, common_xml_safe_str($value));
633 $this->showXmlAttachments($twitter_status['attachments']);
636 $this->showGeoXML($value);
638 case 'retweeted_status':
639 $this->showTwitterXmlStatus($value, 'retweeted_status');
642 if (strncmp($element, 'statusnet_', 10) == 0) {
643 $this->element('statusnet:'.substr($element, 10), null, $value);
645 $this->element($element, null, $value);
649 $this->elementEnd($tag);
652 function showTwitterXmlGroup($twitter_group)
654 $this->elementStart('group');
655 foreach($twitter_group as $element => $value) {
656 $this->element($element, null, $value);
658 $this->elementEnd('group');
661 function showTwitterXmlList($twitter_list)
663 $this->elementStart('list');
664 foreach($twitter_list as $element => $value) {
665 if($element == 'user') {
666 $this->showTwitterXmlUser($value, 'user');
669 $this->element($element, null, $value);
672 $this->elementEnd('list');
675 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
679 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
681 $this->elementStart($role, $attrs);
682 foreach($twitter_user as $element => $value) {
683 if ($element == 'status') {
684 $this->showTwitterXmlStatus($twitter_user['status']);
685 } else if (strncmp($element, 'statusnet_', 10) == 0) {
686 $this->element('statusnet:'.substr($element, 10), null, $value);
688 $this->element($element, null, $value);
691 $this->elementEnd($role);
694 function showXmlAttachments($attachments) {
695 if (!empty($attachments)) {
696 $this->elementStart('attachments', array('type' => 'array'));
697 foreach ($attachments as $attachment) {
699 $attrs['url'] = $attachment['url'];
700 $attrs['mimetype'] = $attachment['mimetype'];
701 $attrs['size'] = $attachment['size'];
702 $this->element('enclosure', $attrs, '');
704 $this->elementEnd('attachments');
708 function showGeoXML($geo)
712 $this->element('geo');
714 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
715 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
716 $this->elementEnd('geo');
720 function showGeoRSS($geo)
726 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
731 function showTwitterRssItem($entry)
733 $this->elementStart('item');
734 $this->element('title', null, $entry['title']);
735 $this->element('description', null, $entry['description']);
736 $this->element('pubDate', null, $entry['pubDate']);
737 $this->element('guid', null, $entry['guid']);
738 $this->element('link', null, $entry['link']);
740 // RSS only supports 1 enclosure per item
741 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
742 $enclosure = $entry['enclosures'][0];
743 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
746 if(array_key_exists('tags', $entry)){
747 foreach($entry['tags'] as $tag){
748 $this->element('category', null,$tag);
752 $this->showGeoRSS($entry['geo']);
753 $this->elementEnd('item');
756 function showJsonObjects($objects)
758 print(json_encode($objects));
761 function showSingleXmlStatus($notice)
763 $this->initDocument('xml');
764 $twitter_status = $this->twitterStatusArray($notice);
765 $this->showTwitterXmlStatus($twitter_status, 'status', true);
766 $this->endDocument('xml');
769 function showSingleAtomStatus($notice)
771 header('Content-Type: application/atom+xml; charset=utf-8');
772 print $notice->asAtomEntry(true, true, true, $this->auth_user);
775 function show_single_json_status($notice)
777 $this->initDocument('json');
778 $status = $this->twitterStatusArray($notice);
779 $this->showJsonObjects($status);
780 $this->endDocument('json');
783 function showXmlTimeline($notice)
785 $this->initDocument('xml');
786 $this->elementStart('statuses', array('type' => 'array',
787 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
789 if (is_array($notice)) {
790 $notice = new ArrayWrapper($notice);
793 while ($notice->fetch()) {
795 $twitter_status = $this->twitterStatusArray($notice);
796 $this->showTwitterXmlStatus($twitter_status);
797 } catch (Exception $e) {
798 common_log(LOG_ERR, $e->getMessage());
803 $this->elementEnd('statuses');
804 $this->endDocument('xml');
807 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
809 $this->initDocument('rss');
811 $this->element('title', null, $title);
812 $this->element('link', null, $link);
814 if (!is_null($self)) {
818 'type' => 'application/rss+xml',
825 if (!is_null($suplink)) {
826 // For FriendFeed's SUP protocol
827 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
828 'rel' => 'http://api.friendfeed.com/2008/03#sup',
830 'type' => 'application/json'));
833 if (!is_null($logo)) {
834 $this->elementStart('image');
835 $this->element('link', null, $link);
836 $this->element('title', null, $title);
837 $this->element('url', null, $logo);
838 $this->elementEnd('image');
841 $this->element('description', null, $subtitle);
842 $this->element('language', null, 'en-us');
843 $this->element('ttl', null, '40');
845 if (is_array($notice)) {
846 $notice = new ArrayWrapper($notice);
849 while ($notice->fetch()) {
851 $entry = $this->twitterRssEntryArray($notice);
852 $this->showTwitterRssItem($entry);
853 } catch (Exception $e) {
854 common_log(LOG_ERR, $e->getMessage());
855 // continue on exceptions
859 $this->endTwitterRss();
862 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
864 $this->initDocument('atom');
866 $this->element('title', null, $title);
867 $this->element('id', null, $id);
868 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
870 if (!is_null($logo)) {
871 $this->element('logo',null,$logo);
874 if (!is_null($suplink)) {
875 // For FriendFeed's SUP protocol
876 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
878 'type' => 'application/json'));
881 if (!is_null($selfuri)) {
882 $this->element('link', array('href' => $selfuri,
883 'rel' => 'self', 'type' => 'application/atom+xml'), null);
886 $this->element('updated', null, common_date_iso8601('now'));
887 $this->element('subtitle', null, $subtitle);
889 if (is_array($notice)) {
890 $notice = new ArrayWrapper($notice);
893 while ($notice->fetch()) {
895 $this->raw($notice->asAtomEntry());
896 } catch (Exception $e) {
897 common_log(LOG_ERR, $e->getMessage());
902 $this->endDocument('atom');
905 function showRssGroups($group, $title, $link, $subtitle)
907 $this->initDocument('rss');
909 $this->element('title', null, $title);
910 $this->element('link', null, $link);
911 $this->element('description', null, $subtitle);
912 $this->element('language', null, 'en-us');
913 $this->element('ttl', null, '40');
915 if (is_array($group)) {
916 foreach ($group as $g) {
917 $twitter_group = $this->twitterRssGroupArray($g);
918 $this->showTwitterRssItem($twitter_group);
921 while ($group->fetch()) {
922 $twitter_group = $this->twitterRssGroupArray($group);
923 $this->showTwitterRssItem($twitter_group);
927 $this->endTwitterRss();
930 function showTwitterAtomEntry($entry)
932 $this->elementStart('entry');
933 $this->element('title', null, common_xml_safe_str($entry['title']));
936 array('type' => 'html'),
937 common_xml_safe_str($entry['content'])
939 $this->element('id', null, $entry['id']);
940 $this->element('published', null, $entry['published']);
941 $this->element('updated', null, $entry['updated']);
942 $this->element('link', array('type' => 'text/html',
943 'href' => $entry['link'],
944 'rel' => 'alternate'));
945 $this->element('link', array('type' => $entry['avatar-type'],
946 'href' => $entry['avatar'],
948 $this->elementStart('author');
950 $this->element('name', null, $entry['author-name']);
951 $this->element('uri', null, $entry['author-uri']);
953 $this->elementEnd('author');
954 $this->elementEnd('entry');
957 function showXmlDirectMessage($dm, $namespaces=false)
961 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
963 $this->elementStart('direct_message', $attrs);
964 foreach($dm as $element => $value) {
968 $this->showTwitterXmlUser($value, $element);
971 $this->element($element, null, common_xml_safe_str($value));
974 $this->element($element, null, $value);
978 $this->elementEnd('direct_message');
981 function directMessageArray($message)
985 $from_profile = $message->getFrom();
986 $to_profile = $message->getTo();
988 $dmsg['id'] = intval($message->id);
989 $dmsg['sender_id'] = intval($from_profile->id);
990 $dmsg['text'] = trim($message->content);
991 $dmsg['recipient_id'] = intval($to_profile->id);
992 $dmsg['created_at'] = $this->dateTwitter($message->created);
993 $dmsg['sender_screen_name'] = $from_profile->nickname;
994 $dmsg['recipient_screen_name'] = $to_profile->nickname;
995 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
996 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1001 function rssDirectMessageArray($message)
1005 $from = $message->getFrom();
1007 $entry['title'] = sprintf('Message from %1$s to %2$s',
1008 $from->nickname, $message->getTo()->nickname);
1010 $entry['content'] = common_xml_safe_str($message->rendered);
1011 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1012 $entry['published'] = common_date_iso8601($message->created);
1014 $taguribase = TagURI::base();
1016 $entry['id'] = "tag:$taguribase:$entry[link]";
1017 $entry['updated'] = $entry['published'];
1019 $entry['author-name'] = $from->getBestName();
1020 $entry['author-uri'] = $from->homepage;
1022 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1024 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1025 $entry['avatar-type'] = $avatar->mediatype;
1026 } catch (Exception $e) {
1027 $entry['avatar-type'] = 'image/png';
1030 // RSS item specific
1032 $entry['description'] = $entry['content'];
1033 $entry['pubDate'] = common_date_rfc2822($message->created);
1034 $entry['guid'] = $entry['link'];
1039 function showSingleXmlDirectMessage($message)
1041 $this->initDocument('xml');
1042 $dmsg = $this->directMessageArray($message);
1043 $this->showXmlDirectMessage($dmsg, true);
1044 $this->endDocument('xml');
1047 function showSingleJsonDirectMessage($message)
1049 $this->initDocument('json');
1050 $dmsg = $this->directMessageArray($message);
1051 $this->showJsonObjects($dmsg);
1052 $this->endDocument('json');
1055 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1057 $this->initDocument('atom');
1059 $this->element('title', null, common_xml_safe_str($title));
1060 $this->element('id', null, $id);
1061 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1063 if (!is_null($selfuri)) {
1064 $this->element('link', array('href' => $selfuri,
1065 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1068 $this->element('updated', null, common_date_iso8601('now'));
1069 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1071 if (is_array($group)) {
1072 foreach ($group as $g) {
1073 $this->raw($g->asAtomEntry());
1076 while ($group->fetch()) {
1077 $this->raw($group->asAtomEntry());
1081 $this->endDocument('atom');
1085 function showJsonTimeline($notice)
1087 $this->initDocument('json');
1089 $statuses = array();
1091 if (is_array($notice)) {
1092 $notice = new ArrayWrapper($notice);
1095 while ($notice->fetch()) {
1097 $twitter_status = $this->twitterStatusArray($notice);
1098 array_push($statuses, $twitter_status);
1099 } catch (Exception $e) {
1100 common_log(LOG_ERR, $e->getMessage());
1105 $this->showJsonObjects($statuses);
1107 $this->endDocument('json');
1110 function showJsonGroups($group)
1112 $this->initDocument('json');
1116 if (is_array($group)) {
1117 foreach ($group as $g) {
1118 $twitter_group = $this->twitterGroupArray($g);
1119 array_push($groups, $twitter_group);
1122 while ($group->fetch()) {
1123 $twitter_group = $this->twitterGroupArray($group);
1124 array_push($groups, $twitter_group);
1128 $this->showJsonObjects($groups);
1130 $this->endDocument('json');
1133 function showXmlGroups($group)
1136 $this->initDocument('xml');
1137 $this->elementStart('groups', array('type' => 'array'));
1139 if (is_array($group)) {
1140 foreach ($group as $g) {
1141 $twitter_group = $this->twitterGroupArray($g);
1142 $this->showTwitterXmlGroup($twitter_group);
1145 while ($group->fetch()) {
1146 $twitter_group = $this->twitterGroupArray($group);
1147 $this->showTwitterXmlGroup($twitter_group);
1151 $this->elementEnd('groups');
1152 $this->endDocument('xml');
1155 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1158 $this->initDocument('xml');
1159 $this->elementStart('lists_list');
1160 $this->elementStart('lists', array('type' => 'array'));
1162 if (is_array($list)) {
1163 foreach ($list as $l) {
1164 $twitter_list = $this->twitterListArray($l);
1165 $this->showTwitterXmlList($twitter_list);
1168 while ($list->fetch()) {
1169 $twitter_list = $this->twitterListArray($list);
1170 $this->showTwitterXmlList($twitter_list);
1174 $this->elementEnd('lists');
1176 $this->element('next_cursor', null, $next_cursor);
1177 $this->element('previous_cursor', null, $prev_cursor);
1179 $this->elementEnd('lists_list');
1180 $this->endDocument('xml');
1183 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1185 $this->initDocument('json');
1189 if (is_array($list)) {
1190 foreach ($list as $l) {
1191 $twitter_list = $this->twitterListArray($l);
1192 array_push($lists, $twitter_list);
1195 while ($list->fetch()) {
1196 $twitter_list = $this->twitterListArray($list);
1197 array_push($lists, $twitter_list);
1201 $lists_list = array(
1203 'next_cursor' => $next_cursor,
1204 'next_cursor_str' => strval($next_cursor),
1205 'previous_cursor' => $prev_cursor,
1206 'previous_cursor_str' => strval($prev_cursor)
1209 $this->showJsonObjects($lists_list);
1211 $this->endDocument('json');
1214 function showTwitterXmlUsers($user)
1216 $this->initDocument('xml');
1217 $this->elementStart('users', array('type' => 'array',
1218 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1220 if (is_array($user)) {
1221 foreach ($user as $u) {
1222 $twitter_user = $this->twitterUserArray($u);
1223 $this->showTwitterXmlUser($twitter_user);
1226 while ($user->fetch()) {
1227 $twitter_user = $this->twitterUserArray($user);
1228 $this->showTwitterXmlUser($twitter_user);
1232 $this->elementEnd('users');
1233 $this->endDocument('xml');
1236 function showJsonUsers($user)
1238 $this->initDocument('json');
1242 if (is_array($user)) {
1243 foreach ($user as $u) {
1244 $twitter_user = $this->twitterUserArray($u);
1245 array_push($users, $twitter_user);
1248 while ($user->fetch()) {
1249 $twitter_user = $this->twitterUserArray($user);
1250 array_push($users, $twitter_user);
1254 $this->showJsonObjects($users);
1256 $this->endDocument('json');
1259 function showSingleJsonGroup($group)
1261 $this->initDocument('json');
1262 $twitter_group = $this->twitterGroupArray($group);
1263 $this->showJsonObjects($twitter_group);
1264 $this->endDocument('json');
1267 function showSingleXmlGroup($group)
1269 $this->initDocument('xml');
1270 $twitter_group = $this->twitterGroupArray($group);
1271 $this->showTwitterXmlGroup($twitter_group);
1272 $this->endDocument('xml');
1275 function showSingleJsonList($list)
1277 $this->initDocument('json');
1278 $twitter_list = $this->twitterListArray($list);
1279 $this->showJsonObjects($twitter_list);
1280 $this->endDocument('json');
1283 function showSingleXmlList($list)
1285 $this->initDocument('xml');
1286 $twitter_list = $this->twitterListArray($list);
1287 $this->showTwitterXmlList($twitter_list);
1288 $this->endDocument('xml');
1291 function dateTwitter($dt)
1293 $dateStr = date('d F Y H:i:s', strtotime($dt));
1294 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1295 $d->setTimezone(new DateTimeZone(common_timezone()));
1296 return $d->format('D M d H:i:s O Y');
1299 function initDocument($type='xml')
1303 header('Content-Type: application/xml; charset=utf-8');
1307 header('Content-Type: application/json; charset=utf-8');
1309 // Check for JSONP callback
1310 if (isset($this->callback)) {
1311 print $this->callback . '(';
1315 header("Content-Type: application/rss+xml; charset=utf-8");
1316 $this->initTwitterRss();
1319 header('Content-Type: application/atom+xml; charset=utf-8');
1320 $this->initTwitterAtom();
1323 // TRANS: Client error on an API request with an unsupported data format.
1324 $this->clientError(_('Not a supported data format.'));
1331 function endDocument($type='xml')
1338 // Check for JSONP callback
1339 if (isset($this->callback)) {
1344 $this->endTwitterRss();
1347 $this->endTwitterRss();
1350 // TRANS: Client error on an API request with an unsupported data format.
1351 $this->clientError(_('Not a supported data format.'));
1357 function initTwitterRss()
1360 $this->elementStart(
1364 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1365 'xmlns:georss' => 'http://www.georss.org/georss'
1368 $this->elementStart('channel');
1369 Event::handle('StartApiRss', array($this));
1372 function endTwitterRss()
1374 $this->elementEnd('channel');
1375 $this->elementEnd('rss');
1379 function initTwitterAtom()
1382 // FIXME: don't hardcode the language here!
1383 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1384 'xml:lang' => 'en-US',
1385 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1388 function endTwitterAtom()
1390 $this->elementEnd('feed');
1394 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1396 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1397 switch ($content_type) {
1399 $this->showTwitterXmlUser($profile_array);
1402 $this->showJsonObjects($profile_array);
1405 // TRANS: Client error on an API request with an unsupported data format.
1406 $this->clientError(_('Not a supported data format.'));
1412 private static function is_decimal($str)
1414 return preg_match('/^[0-9]+$/', $str);
1417 function getTargetUser($id)
1420 // Twitter supports these other ways of passing the user ID
1421 if (self::is_decimal($this->arg('id'))) {
1422 return User::getKV($this->arg('id'));
1423 } else if ($this->arg('id')) {
1424 $nickname = common_canonical_nickname($this->arg('id'));
1425 return User::getKV('nickname', $nickname);
1426 } else if ($this->arg('user_id')) {
1427 // This is to ensure that a non-numeric user_id still
1428 // overrides screen_name even if it doesn't get used
1429 if (self::is_decimal($this->arg('user_id'))) {
1430 return User::getKV('id', $this->arg('user_id'));
1432 } else if ($this->arg('screen_name')) {
1433 $nickname = common_canonical_nickname($this->arg('screen_name'));
1434 return User::getKV('nickname', $nickname);
1436 // Fall back to trying the currently authenticated user
1437 return $this->auth_user;
1440 } else if (self::is_decimal($id)) {
1441 return User::getKV($id);
1443 $nickname = common_canonical_nickname($id);
1444 return User::getKV('nickname', $nickname);
1448 function getTargetProfile($id)
1452 // Twitter supports these other ways of passing the user ID
1453 if (self::is_decimal($this->arg('id'))) {
1454 return Profile::getKV($this->arg('id'));
1455 } else if ($this->arg('id')) {
1456 // Screen names currently can only uniquely identify a local user.
1457 $nickname = common_canonical_nickname($this->arg('id'));
1458 $user = User::getKV('nickname', $nickname);
1459 return $user ? $user->getProfile() : null;
1460 } else if ($this->arg('user_id')) {
1461 // This is to ensure that a non-numeric user_id still
1462 // overrides screen_name even if it doesn't get used
1463 if (self::is_decimal($this->arg('user_id'))) {
1464 return Profile::getKV('id', $this->arg('user_id'));
1466 } else if ($this->arg('screen_name')) {
1467 $nickname = common_canonical_nickname($this->arg('screen_name'));
1468 $user = User::getKV('nickname', $nickname);
1469 return $user ? $user->getProfile() : null;
1471 // Fall back to trying the currently authenticated user
1472 return $this->scoped;
1474 } else if (self::is_decimal($id)) {
1475 return Profile::getKV($id);
1477 $nickname = common_canonical_nickname($id);
1478 $user = User::getKV('nickname', $nickname);
1479 return $user ? $user->getProfile() : null;
1483 function getTargetGroup($id)
1486 if (self::is_decimal($this->arg('id'))) {
1487 return User_group::getKV('id', $this->arg('id'));
1488 } else if ($this->arg('id')) {
1489 return User_group::getForNickname($this->arg('id'));
1490 } else if ($this->arg('group_id')) {
1491 // This is to ensure that a non-numeric group_id still
1492 // overrides group_name even if it doesn't get used
1493 if (self::is_decimal($this->arg('group_id'))) {
1494 return User_group::getKV('id', $this->arg('group_id'));
1496 } else if ($this->arg('group_name')) {
1497 return User_group::getForNickname($this->arg('group_name'));
1500 } else if (self::is_decimal($id)) {
1501 return User_group::getKV('id', $id);
1502 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1503 return User_group::getKV('uri', urldecode($this->arg('uri')));
1505 return User_group::getForNickname($id);
1509 function getTargetList($user=null, $id=null)
1511 $tagger = $this->getTargetUser($user);
1515 $id = $this->arg('id');
1519 if (is_numeric($id)) {
1520 $list = Profile_list::getKV('id', $id);
1522 // only if the list with the id belongs to the tagger
1523 if(empty($list) || $list->tagger != $tagger->id) {
1528 $tag = common_canonical_tag($id);
1529 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1532 if (!empty($list) && $list->private) {
1533 if ($this->auth_user->id == $list->tagger) {
1544 * Returns query argument or default value if not found. Certain
1545 * parameters used throughout the API are lightly scrubbed and
1546 * bounds checked. This overrides Action::arg().
1548 * @param string $key requested argument
1549 * @param string $def default value to return if $key is not provided
1553 function arg($key, $def=null)
1555 // XXX: Do even more input validation/scrubbing?
1557 if (array_key_exists($key, $this->args)) {
1560 $page = (int)$this->args['page'];
1561 return ($page < 1) ? 1 : $page;
1563 $count = (int)$this->args['count'];
1566 } elseif ($count > 200) {
1572 $since_id = (int)$this->args['since_id'];
1573 return ($since_id < 1) ? 0 : $since_id;
1575 $max_id = (int)$this->args['max_id'];
1576 return ($max_id < 1) ? 0 : $max_id;
1578 return parent::arg($key, $def);
1586 * Calculate the complete URI that called up this action. Used for
1587 * Atom rel="self" links. Warning: this is funky.
1589 * @return string URL a URL suitable for rel="self" Atom links
1591 function getSelfUri()
1593 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1595 $id = $this->arg('id');
1596 $aargs = array('format' => $this->format);
1601 $tag = $this->arg('tag');
1603 $aargs['tag'] = $tag;
1606 parse_str($_SERVER['QUERY_STRING'], $params);
1608 if (!empty($params)) {
1609 unset($params['p']);
1610 $pstring = http_build_query($params);
1613 $uri = common_local_url($action, $aargs);
1615 if (!empty($pstring)) {
1616 $uri .= '?' . $pstring;