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->getGroups(0, null)->N;
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 $in_reply_to = $notice->getParent()->id;
319 } catch (Exception $e) {
322 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
326 $ns = $notice->getSource();
328 if (!empty($ns->name) && !empty($ns->url)) {
329 $source = '<a href="'
330 . htmlspecialchars($ns->url)
331 . '" rel="nofollow">'
332 . htmlspecialchars($ns->name)
339 $twitter_status['uri'] = $notice->getUri();
340 $twitter_status['source'] = $source;
341 $twitter_status['id'] = intval($notice->id);
343 $replier_profile = null;
345 if ($notice->reply_to) {
346 $reply = Notice::getKV(intval($notice->reply_to));
348 $replier_profile = $reply->getProfile();
352 $twitter_status['in_reply_to_user_id'] =
353 ($replier_profile) ? intval($replier_profile->id) : null;
354 $twitter_status['in_reply_to_screen_name'] =
355 ($replier_profile) ? $replier_profile->nickname : null;
357 if (isset($notice->lat) && isset($notice->lon)) {
358 // This is the format that GeoJSON expects stuff to be in
359 $twitter_status['geo'] = array('type' => 'Point',
360 'coordinates' => array((float) $notice->lat,
361 (float) $notice->lon));
363 $twitter_status['geo'] = null;
366 if (!is_null($this->scoped)) {
367 $twitter_status['favorited'] = $this->scoped->hasFave($notice);
368 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
370 $twitter_status['favorited'] = false;
371 $twitter_status['repeated'] = false;
375 $attachments = $notice->attachments();
377 if (!empty($attachments)) {
379 $twitter_status['attachments'] = array();
381 foreach ($attachments as $attachment) {
382 $enclosure_o=$attachment->getEnclosure();
384 $enclosure = array();
385 $enclosure['url'] = $enclosure_o->url;
386 $enclosure['mimetype'] = $enclosure_o->mimetype;
387 $enclosure['size'] = $enclosure_o->size;
388 $twitter_status['attachments'][] = $enclosure;
393 if ($include_user && $profile) {
394 // Don't get notice (recursive!)
395 $twitter_user = $this->twitterUserArray($profile, false);
396 $twitter_status['user'] = $twitter_user;
399 // StatusNet-specific
401 $twitter_status['statusnet_html'] = $notice->rendered;
402 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
404 return $twitter_status;
407 function twitterGroupArray($group)
409 $twitter_group = array();
411 $twitter_group['id'] = intval($group->id);
412 $twitter_group['url'] = $group->permalink();
413 $twitter_group['nickname'] = $group->nickname;
414 $twitter_group['fullname'] = $group->fullname;
416 if (isset($this->auth_user)) {
417 $twitter_group['member'] = $this->auth_user->isMember($group);
418 $twitter_group['blocked'] = Group_block::isBlocked(
420 $this->auth_user->getProfile()
424 $twitter_group['admin_count'] = $group->getAdminCount();
425 $twitter_group['member_count'] = $group->getMemberCount();
426 $twitter_group['original_logo'] = $group->original_logo;
427 $twitter_group['homepage_logo'] = $group->homepage_logo;
428 $twitter_group['stream_logo'] = $group->stream_logo;
429 $twitter_group['mini_logo'] = $group->mini_logo;
430 $twitter_group['homepage'] = $group->homepage;
431 $twitter_group['description'] = $group->description;
432 $twitter_group['location'] = $group->location;
433 $twitter_group['created'] = $this->dateTwitter($group->created);
434 $twitter_group['modified'] = $this->dateTwitter($group->modified);
436 return $twitter_group;
439 function twitterRssGroupArray($group)
442 $entry['content']=$group->description;
443 $entry['title']=$group->nickname;
444 $entry['link']=$group->permalink();
445 $entry['published']=common_date_iso8601($group->created);
446 $entry['updated']==common_date_iso8601($group->modified);
447 $taguribase = common_config('integration', 'groupuri');
448 $entry['id'] = "group:$groupuribase:$entry[link]";
450 $entry['description'] = $entry['content'];
451 $entry['pubDate'] = common_date_rfc2822($group->created);
452 $entry['guid'] = $entry['link'];
457 function twitterListArray($list)
459 $profile = Profile::getKV('id', $list->tagger);
461 $twitter_list = array();
462 $twitter_list['id'] = $list->id;
463 $twitter_list['name'] = $list->tag;
464 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
465 $twitter_list['slug'] = $list->tag;
466 $twitter_list['description'] = $list->description;
467 $twitter_list['subscriber_count'] = $list->subscriberCount();
468 $twitter_list['member_count'] = $list->taggedCount();
469 $twitter_list['uri'] = $list->getUri();
471 if (isset($this->auth_user)) {
472 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
474 $twitter_list['following'] = false;
477 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
478 $twitter_list['user'] = $this->twitterUserArray($profile, false);
480 return $twitter_list;
483 function twitterRssEntryArray($notice)
487 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
488 $profile = $notice->getProfile();
490 // We trim() to avoid extraneous whitespace in the output
492 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
493 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
494 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
495 $entry['published'] = common_date_iso8601($notice->created);
497 $taguribase = TagURI::base();
498 $entry['id'] = "tag:$taguribase:$entry[link]";
500 $entry['updated'] = $entry['published'];
501 $entry['author'] = $profile->getBestName();
504 $attachments = $notice->attachments();
505 $enclosures = array();
507 foreach ($attachments as $attachment) {
508 $enclosure_o=$attachment->getEnclosure();
510 $enclosure = array();
511 $enclosure['url'] = $enclosure_o->url;
512 $enclosure['mimetype'] = $enclosure_o->mimetype;
513 $enclosure['size'] = $enclosure_o->size;
514 $enclosures[] = $enclosure;
518 if (!empty($enclosures)) {
519 $entry['enclosures'] = $enclosures;
523 $tag = new Notice_tag();
524 $tag->notice_id = $notice->id;
526 $entry['tags']=array();
527 while ($tag->fetch()) {
528 $entry['tags'][]=$tag->tag;
534 $entry['description'] = $entry['content'];
535 $entry['pubDate'] = common_date_rfc2822($notice->created);
536 $entry['guid'] = $entry['link'];
538 if (isset($notice->lat) && isset($notice->lon)) {
539 // This is the format that GeoJSON expects stuff to be in.
540 // showGeoRSS() below uses it for XML output, so we reuse it
541 $entry['geo'] = array('type' => 'Point',
542 'coordinates' => array((float) $notice->lat,
543 (float) $notice->lon));
545 $entry['geo'] = null;
548 Event::handle('EndRssEntryArray', array($notice, &$entry));
554 function twitterRelationshipArray($source, $target)
556 $relationship = array();
558 $relationship['source'] =
559 $this->relationshipDetailsArray($source, $target);
560 $relationship['target'] =
561 $this->relationshipDetailsArray($target, $source);
563 return array('relationship' => $relationship);
566 function relationshipDetailsArray($source, $target)
570 $details['screen_name'] = $source->nickname;
571 $details['followed_by'] = $target->isSubscribed($source);
572 $details['following'] = $source->isSubscribed($target);
574 $notifications = false;
576 if ($source->isSubscribed($target)) {
577 $sub = Subscription::pkeyGet(array('subscriber' =>
578 $source->id, 'subscribed' => $target->id));
581 $notifications = ($sub->jabber || $sub->sms);
585 $details['notifications_enabled'] = $notifications;
586 $details['blocking'] = $source->hasBlocked($target);
587 $details['id'] = intval($source->id);
592 function showTwitterXmlRelationship($relationship)
594 $this->elementStart('relationship');
596 foreach($relationship as $element => $value) {
597 if ($element == 'source' || $element == 'target') {
598 $this->elementStart($element);
599 $this->showXmlRelationshipDetails($value);
600 $this->elementEnd($element);
604 $this->elementEnd('relationship');
607 function showXmlRelationshipDetails($details)
609 foreach($details as $element => $value) {
610 $this->element($element, null, $value);
614 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
618 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
620 $this->elementStart($tag, $attrs);
621 foreach($twitter_status as $element => $value) {
624 $this->showTwitterXmlUser($twitter_status['user']);
627 $this->element($element, null, common_xml_safe_str($value));
630 $this->showXmlAttachments($twitter_status['attachments']);
633 $this->showGeoXML($value);
635 case 'retweeted_status':
636 $this->showTwitterXmlStatus($value, 'retweeted_status');
639 if (strncmp($element, 'statusnet_', 10) == 0) {
640 $this->element('statusnet:'.substr($element, 10), null, $value);
642 $this->element($element, null, $value);
646 $this->elementEnd($tag);
649 function showTwitterXmlGroup($twitter_group)
651 $this->elementStart('group');
652 foreach($twitter_group as $element => $value) {
653 $this->element($element, null, $value);
655 $this->elementEnd('group');
658 function showTwitterXmlList($twitter_list)
660 $this->elementStart('list');
661 foreach($twitter_list as $element => $value) {
662 if($element == 'user') {
663 $this->showTwitterXmlUser($value, 'user');
666 $this->element($element, null, $value);
669 $this->elementEnd('list');
672 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
676 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
678 $this->elementStart($role, $attrs);
679 foreach($twitter_user as $element => $value) {
680 if ($element == 'status') {
681 $this->showTwitterXmlStatus($twitter_user['status']);
682 } else if (strncmp($element, 'statusnet_', 10) == 0) {
683 $this->element('statusnet:'.substr($element, 10), null, $value);
685 $this->element($element, null, $value);
688 $this->elementEnd($role);
691 function showXmlAttachments($attachments) {
692 if (!empty($attachments)) {
693 $this->elementStart('attachments', array('type' => 'array'));
694 foreach ($attachments as $attachment) {
696 $attrs['url'] = $attachment['url'];
697 $attrs['mimetype'] = $attachment['mimetype'];
698 $attrs['size'] = $attachment['size'];
699 $this->element('enclosure', $attrs, '');
701 $this->elementEnd('attachments');
705 function showGeoXML($geo)
709 $this->element('geo');
711 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
712 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
713 $this->elementEnd('geo');
717 function showGeoRSS($geo)
723 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
728 function showTwitterRssItem($entry)
730 $this->elementStart('item');
731 $this->element('title', null, $entry['title']);
732 $this->element('description', null, $entry['description']);
733 $this->element('pubDate', null, $entry['pubDate']);
734 $this->element('guid', null, $entry['guid']);
735 $this->element('link', null, $entry['link']);
737 // RSS only supports 1 enclosure per item
738 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
739 $enclosure = $entry['enclosures'][0];
740 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
743 if(array_key_exists('tags', $entry)){
744 foreach($entry['tags'] as $tag){
745 $this->element('category', null,$tag);
749 $this->showGeoRSS($entry['geo']);
750 $this->elementEnd('item');
753 function showJsonObjects($objects)
755 print(json_encode($objects));
758 function showSingleXmlStatus($notice)
760 $this->initDocument('xml');
761 $twitter_status = $this->twitterStatusArray($notice);
762 $this->showTwitterXmlStatus($twitter_status, 'status', true);
763 $this->endDocument('xml');
766 function showSingleAtomStatus($notice)
768 header('Content-Type: application/atom+xml; charset=utf-8');
769 print $notice->asAtomEntry(true, true, true, $this->auth_user);
772 function show_single_json_status($notice)
774 $this->initDocument('json');
775 $status = $this->twitterStatusArray($notice);
776 $this->showJsonObjects($status);
777 $this->endDocument('json');
780 function showXmlTimeline($notice)
782 $this->initDocument('xml');
783 $this->elementStart('statuses', array('type' => 'array',
784 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
786 if (is_array($notice)) {
787 $notice = new ArrayWrapper($notice);
790 while ($notice->fetch()) {
792 $twitter_status = $this->twitterStatusArray($notice);
793 $this->showTwitterXmlStatus($twitter_status);
794 } catch (Exception $e) {
795 common_log(LOG_ERR, $e->getMessage());
800 $this->elementEnd('statuses');
801 $this->endDocument('xml');
804 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
806 $this->initDocument('rss');
808 $this->element('title', null, $title);
809 $this->element('link', null, $link);
811 if (!is_null($self)) {
815 'type' => 'application/rss+xml',
822 if (!is_null($suplink)) {
823 // For FriendFeed's SUP protocol
824 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
825 'rel' => 'http://api.friendfeed.com/2008/03#sup',
827 'type' => 'application/json'));
830 if (!is_null($logo)) {
831 $this->elementStart('image');
832 $this->element('link', null, $link);
833 $this->element('title', null, $title);
834 $this->element('url', null, $logo);
835 $this->elementEnd('image');
838 $this->element('description', null, $subtitle);
839 $this->element('language', null, 'en-us');
840 $this->element('ttl', null, '40');
842 if (is_array($notice)) {
843 $notice = new ArrayWrapper($notice);
846 while ($notice->fetch()) {
848 $entry = $this->twitterRssEntryArray($notice);
849 $this->showTwitterRssItem($entry);
850 } catch (Exception $e) {
851 common_log(LOG_ERR, $e->getMessage());
852 // continue on exceptions
856 $this->endTwitterRss();
859 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
861 $this->initDocument('atom');
863 $this->element('title', null, $title);
864 $this->element('id', null, $id);
865 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
867 if (!is_null($logo)) {
868 $this->element('logo',null,$logo);
871 if (!is_null($suplink)) {
872 // For FriendFeed's SUP protocol
873 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
875 'type' => 'application/json'));
878 if (!is_null($selfuri)) {
879 $this->element('link', array('href' => $selfuri,
880 'rel' => 'self', 'type' => 'application/atom+xml'), null);
883 $this->element('updated', null, common_date_iso8601('now'));
884 $this->element('subtitle', null, $subtitle);
886 if (is_array($notice)) {
887 $notice = new ArrayWrapper($notice);
890 while ($notice->fetch()) {
892 $this->raw($notice->asAtomEntry());
893 } catch (Exception $e) {
894 common_log(LOG_ERR, $e->getMessage());
899 $this->endDocument('atom');
902 function showRssGroups($group, $title, $link, $subtitle)
904 $this->initDocument('rss');
906 $this->element('title', null, $title);
907 $this->element('link', null, $link);
908 $this->element('description', null, $subtitle);
909 $this->element('language', null, 'en-us');
910 $this->element('ttl', null, '40');
912 if (is_array($group)) {
913 foreach ($group as $g) {
914 $twitter_group = $this->twitterRssGroupArray($g);
915 $this->showTwitterRssItem($twitter_group);
918 while ($group->fetch()) {
919 $twitter_group = $this->twitterRssGroupArray($group);
920 $this->showTwitterRssItem($twitter_group);
924 $this->endTwitterRss();
927 function showTwitterAtomEntry($entry)
929 $this->elementStart('entry');
930 $this->element('title', null, common_xml_safe_str($entry['title']));
933 array('type' => 'html'),
934 common_xml_safe_str($entry['content'])
936 $this->element('id', null, $entry['id']);
937 $this->element('published', null, $entry['published']);
938 $this->element('updated', null, $entry['updated']);
939 $this->element('link', array('type' => 'text/html',
940 'href' => $entry['link'],
941 'rel' => 'alternate'));
942 $this->element('link', array('type' => $entry['avatar-type'],
943 'href' => $entry['avatar'],
945 $this->elementStart('author');
947 $this->element('name', null, $entry['author-name']);
948 $this->element('uri', null, $entry['author-uri']);
950 $this->elementEnd('author');
951 $this->elementEnd('entry');
954 function showXmlDirectMessage($dm, $namespaces=false)
958 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
960 $this->elementStart('direct_message', $attrs);
961 foreach($dm as $element => $value) {
965 $this->showTwitterXmlUser($value, $element);
968 $this->element($element, null, common_xml_safe_str($value));
971 $this->element($element, null, $value);
975 $this->elementEnd('direct_message');
978 function directMessageArray($message)
982 $from_profile = $message->getFrom();
983 $to_profile = $message->getTo();
985 $dmsg['id'] = intval($message->id);
986 $dmsg['sender_id'] = intval($from_profile->id);
987 $dmsg['text'] = trim($message->content);
988 $dmsg['recipient_id'] = intval($to_profile->id);
989 $dmsg['created_at'] = $this->dateTwitter($message->created);
990 $dmsg['sender_screen_name'] = $from_profile->nickname;
991 $dmsg['recipient_screen_name'] = $to_profile->nickname;
992 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
993 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
998 function rssDirectMessageArray($message)
1002 $from = $message->getFrom();
1004 $entry['title'] = sprintf('Message from %1$s to %2$s',
1005 $from->nickname, $message->getTo()->nickname);
1007 $entry['content'] = common_xml_safe_str($message->rendered);
1008 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1009 $entry['published'] = common_date_iso8601($message->created);
1011 $taguribase = TagURI::base();
1013 $entry['id'] = "tag:$taguribase:$entry[link]";
1014 $entry['updated'] = $entry['published'];
1016 $entry['author-name'] = $from->getBestName();
1017 $entry['author-uri'] = $from->homepage;
1019 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1021 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1022 $entry['avatar-type'] = $avatar->mediatype;
1023 } catch (Exception $e) {
1024 $entry['avatar-type'] = 'image/png';
1027 // RSS item specific
1029 $entry['description'] = $entry['content'];
1030 $entry['pubDate'] = common_date_rfc2822($message->created);
1031 $entry['guid'] = $entry['link'];
1036 function showSingleXmlDirectMessage($message)
1038 $this->initDocument('xml');
1039 $dmsg = $this->directMessageArray($message);
1040 $this->showXmlDirectMessage($dmsg, true);
1041 $this->endDocument('xml');
1044 function showSingleJsonDirectMessage($message)
1046 $this->initDocument('json');
1047 $dmsg = $this->directMessageArray($message);
1048 $this->showJsonObjects($dmsg);
1049 $this->endDocument('json');
1052 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1054 $this->initDocument('atom');
1056 $this->element('title', null, common_xml_safe_str($title));
1057 $this->element('id', null, $id);
1058 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1060 if (!is_null($selfuri)) {
1061 $this->element('link', array('href' => $selfuri,
1062 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1065 $this->element('updated', null, common_date_iso8601('now'));
1066 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1068 if (is_array($group)) {
1069 foreach ($group as $g) {
1070 $this->raw($g->asAtomEntry());
1073 while ($group->fetch()) {
1074 $this->raw($group->asAtomEntry());
1078 $this->endDocument('atom');
1082 function showJsonTimeline($notice)
1084 $this->initDocument('json');
1086 $statuses = array();
1088 if (is_array($notice)) {
1089 $notice = new ArrayWrapper($notice);
1092 while ($notice->fetch()) {
1094 $twitter_status = $this->twitterStatusArray($notice);
1095 array_push($statuses, $twitter_status);
1096 } catch (Exception $e) {
1097 common_log(LOG_ERR, $e->getMessage());
1102 $this->showJsonObjects($statuses);
1104 $this->endDocument('json');
1107 function showJsonGroups($group)
1109 $this->initDocument('json');
1113 if (is_array($group)) {
1114 foreach ($group as $g) {
1115 $twitter_group = $this->twitterGroupArray($g);
1116 array_push($groups, $twitter_group);
1119 while ($group->fetch()) {
1120 $twitter_group = $this->twitterGroupArray($group);
1121 array_push($groups, $twitter_group);
1125 $this->showJsonObjects($groups);
1127 $this->endDocument('json');
1130 function showXmlGroups($group)
1133 $this->initDocument('xml');
1134 $this->elementStart('groups', array('type' => 'array'));
1136 if (is_array($group)) {
1137 foreach ($group as $g) {
1138 $twitter_group = $this->twitterGroupArray($g);
1139 $this->showTwitterXmlGroup($twitter_group);
1142 while ($group->fetch()) {
1143 $twitter_group = $this->twitterGroupArray($group);
1144 $this->showTwitterXmlGroup($twitter_group);
1148 $this->elementEnd('groups');
1149 $this->endDocument('xml');
1152 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1155 $this->initDocument('xml');
1156 $this->elementStart('lists_list');
1157 $this->elementStart('lists', array('type' => 'array'));
1159 if (is_array($list)) {
1160 foreach ($list as $l) {
1161 $twitter_list = $this->twitterListArray($l);
1162 $this->showTwitterXmlList($twitter_list);
1165 while ($list->fetch()) {
1166 $twitter_list = $this->twitterListArray($list);
1167 $this->showTwitterXmlList($twitter_list);
1171 $this->elementEnd('lists');
1173 $this->element('next_cursor', null, $next_cursor);
1174 $this->element('previous_cursor', null, $prev_cursor);
1176 $this->elementEnd('lists_list');
1177 $this->endDocument('xml');
1180 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1182 $this->initDocument('json');
1186 if (is_array($list)) {
1187 foreach ($list as $l) {
1188 $twitter_list = $this->twitterListArray($l);
1189 array_push($lists, $twitter_list);
1192 while ($list->fetch()) {
1193 $twitter_list = $this->twitterListArray($list);
1194 array_push($lists, $twitter_list);
1198 $lists_list = array(
1200 'next_cursor' => $next_cursor,
1201 'next_cursor_str' => strval($next_cursor),
1202 'previous_cursor' => $prev_cursor,
1203 'previous_cursor_str' => strval($prev_cursor)
1206 $this->showJsonObjects($lists_list);
1208 $this->endDocument('json');
1211 function showTwitterXmlUsers($user)
1213 $this->initDocument('xml');
1214 $this->elementStart('users', array('type' => 'array',
1215 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1217 if (is_array($user)) {
1218 foreach ($user as $u) {
1219 $twitter_user = $this->twitterUserArray($u);
1220 $this->showTwitterXmlUser($twitter_user);
1223 while ($user->fetch()) {
1224 $twitter_user = $this->twitterUserArray($user);
1225 $this->showTwitterXmlUser($twitter_user);
1229 $this->elementEnd('users');
1230 $this->endDocument('xml');
1233 function showJsonUsers($user)
1235 $this->initDocument('json');
1239 if (is_array($user)) {
1240 foreach ($user as $u) {
1241 $twitter_user = $this->twitterUserArray($u);
1242 array_push($users, $twitter_user);
1245 while ($user->fetch()) {
1246 $twitter_user = $this->twitterUserArray($user);
1247 array_push($users, $twitter_user);
1251 $this->showJsonObjects($users);
1253 $this->endDocument('json');
1256 function showSingleJsonGroup($group)
1258 $this->initDocument('json');
1259 $twitter_group = $this->twitterGroupArray($group);
1260 $this->showJsonObjects($twitter_group);
1261 $this->endDocument('json');
1264 function showSingleXmlGroup($group)
1266 $this->initDocument('xml');
1267 $twitter_group = $this->twitterGroupArray($group);
1268 $this->showTwitterXmlGroup($twitter_group);
1269 $this->endDocument('xml');
1272 function showSingleJsonList($list)
1274 $this->initDocument('json');
1275 $twitter_list = $this->twitterListArray($list);
1276 $this->showJsonObjects($twitter_list);
1277 $this->endDocument('json');
1280 function showSingleXmlList($list)
1282 $this->initDocument('xml');
1283 $twitter_list = $this->twitterListArray($list);
1284 $this->showTwitterXmlList($twitter_list);
1285 $this->endDocument('xml');
1288 function dateTwitter($dt)
1290 $dateStr = date('d F Y H:i:s', strtotime($dt));
1291 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1292 $d->setTimezone(new DateTimeZone(common_timezone()));
1293 return $d->format('D M d H:i:s O Y');
1296 function initDocument($type='xml')
1300 header('Content-Type: application/xml; charset=utf-8');
1304 header('Content-Type: application/json; charset=utf-8');
1306 // Check for JSONP callback
1307 if (isset($this->callback)) {
1308 print $this->callback . '(';
1312 header("Content-Type: application/rss+xml; charset=utf-8");
1313 $this->initTwitterRss();
1316 header('Content-Type: application/atom+xml; charset=utf-8');
1317 $this->initTwitterAtom();
1320 // TRANS: Client error on an API request with an unsupported data format.
1321 $this->clientError(_('Not a supported data format.'));
1328 function endDocument($type='xml')
1335 // Check for JSONP callback
1336 if (isset($this->callback)) {
1341 $this->endTwitterRss();
1344 $this->endTwitterRss();
1347 // TRANS: Client error on an API request with an unsupported data format.
1348 $this->clientError(_('Not a supported data format.'));
1354 function initTwitterRss()
1357 $this->elementStart(
1361 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1362 'xmlns:georss' => 'http://www.georss.org/georss'
1365 $this->elementStart('channel');
1366 Event::handle('StartApiRss', array($this));
1369 function endTwitterRss()
1371 $this->elementEnd('channel');
1372 $this->elementEnd('rss');
1376 function initTwitterAtom()
1379 // FIXME: don't hardcode the language here!
1380 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1381 'xml:lang' => 'en-US',
1382 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1385 function endTwitterAtom()
1387 $this->elementEnd('feed');
1391 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1393 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1394 switch ($content_type) {
1396 $this->showTwitterXmlUser($profile_array);
1399 $this->showJsonObjects($profile_array);
1402 // TRANS: Client error on an API request with an unsupported data format.
1403 $this->clientError(_('Not a supported data format.'));
1409 private static function is_decimal($str)
1411 return preg_match('/^[0-9]+$/', $str);
1414 function getTargetUser($id)
1417 // Twitter supports these other ways of passing the user ID
1418 if (self::is_decimal($this->arg('id'))) {
1419 return User::getKV($this->arg('id'));
1420 } else if ($this->arg('id')) {
1421 $nickname = common_canonical_nickname($this->arg('id'));
1422 return User::getKV('nickname', $nickname);
1423 } else if ($this->arg('user_id')) {
1424 // This is to ensure that a non-numeric user_id still
1425 // overrides screen_name even if it doesn't get used
1426 if (self::is_decimal($this->arg('user_id'))) {
1427 return User::getKV('id', $this->arg('user_id'));
1429 } else if ($this->arg('screen_name')) {
1430 $nickname = common_canonical_nickname($this->arg('screen_name'));
1431 return User::getKV('nickname', $nickname);
1433 // Fall back to trying the currently authenticated user
1434 return $this->auth_user;
1437 } else if (self::is_decimal($id)) {
1438 return User::getKV($id);
1440 $nickname = common_canonical_nickname($id);
1441 return User::getKV('nickname', $nickname);
1445 function getTargetProfile($id)
1449 // Twitter supports these other ways of passing the user ID
1450 if (self::is_decimal($this->arg('id'))) {
1451 return Profile::getKV($this->arg('id'));
1452 } else if ($this->arg('id')) {
1453 // Screen names currently can only uniquely identify a local user.
1454 $nickname = common_canonical_nickname($this->arg('id'));
1455 $user = User::getKV('nickname', $nickname);
1456 return $user ? $user->getProfile() : null;
1457 } else if ($this->arg('user_id')) {
1458 // This is to ensure that a non-numeric user_id still
1459 // overrides screen_name even if it doesn't get used
1460 if (self::is_decimal($this->arg('user_id'))) {
1461 return Profile::getKV('id', $this->arg('user_id'));
1463 } else if ($this->arg('screen_name')) {
1464 $nickname = common_canonical_nickname($this->arg('screen_name'));
1465 $user = User::getKV('nickname', $nickname);
1466 return $user ? $user->getProfile() : null;
1468 // Fall back to trying the currently authenticated user
1469 return $this->scoped;
1471 } else if (self::is_decimal($id)) {
1472 return Profile::getKV($id);
1474 $nickname = common_canonical_nickname($id);
1475 $user = User::getKV('nickname', $nickname);
1476 return $user ? $user->getProfile() : null;
1480 function getTargetGroup($id)
1483 if (self::is_decimal($this->arg('id'))) {
1484 return User_group::getKV('id', $this->arg('id'));
1485 } else if ($this->arg('id')) {
1486 return User_group::getForNickname($this->arg('id'));
1487 } else if ($this->arg('group_id')) {
1488 // This is to ensure that a non-numeric group_id still
1489 // overrides group_name even if it doesn't get used
1490 if (self::is_decimal($this->arg('group_id'))) {
1491 return User_group::getKV('id', $this->arg('group_id'));
1493 } else if ($this->arg('group_name')) {
1494 return User_group::getForNickname($this->arg('group_name'));
1497 } else if (self::is_decimal($id)) {
1498 return User_group::getKV('id', $id);
1499 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1500 return User_group::getKV('uri', urldecode($this->arg('uri')));
1502 return User_group::getForNickname($id);
1506 function getTargetList($user=null, $id=null)
1508 $tagger = $this->getTargetUser($user);
1512 $id = $this->arg('id');
1516 if (is_numeric($id)) {
1517 $list = Profile_list::getKV('id', $id);
1519 // only if the list with the id belongs to the tagger
1520 if(empty($list) || $list->tagger != $tagger->id) {
1525 $tag = common_canonical_tag($id);
1526 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1529 if (!empty($list) && $list->private) {
1530 if ($this->auth_user->id == $list->tagger) {
1541 * Returns query argument or default value if not found. Certain
1542 * parameters used throughout the API are lightly scrubbed and
1543 * bounds checked. This overrides Action::arg().
1545 * @param string $key requested argument
1546 * @param string $def default value to return if $key is not provided
1550 function arg($key, $def=null)
1552 // XXX: Do even more input validation/scrubbing?
1554 if (array_key_exists($key, $this->args)) {
1557 $page = (int)$this->args['page'];
1558 return ($page < 1) ? 1 : $page;
1560 $count = (int)$this->args['count'];
1563 } elseif ($count > 200) {
1569 $since_id = (int)$this->args['since_id'];
1570 return ($since_id < 1) ? 0 : $since_id;
1572 $max_id = (int)$this->args['max_id'];
1573 return ($max_id < 1) ? 0 : $max_id;
1575 return parent::arg($key, $def);
1583 * Calculate the complete URI that called up this action. Used for
1584 * Atom rel="self" links. Warning: this is funky.
1586 * @return string URL a URL suitable for rel="self" Atom links
1588 function getSelfUri()
1590 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1592 $id = $this->arg('id');
1593 $aargs = array('format' => $this->format);
1598 $tag = $this->arg('tag');
1600 $aargs['tag'] = $tag;
1603 parse_str($_SERVER['QUERY_STRING'], $params);
1605 if (!empty($params)) {
1606 unset($params['p']);
1607 $pstring = http_build_query($params);
1610 $uri = common_local_url($action, $aargs);
1612 if (!empty($pstring)) {
1613 $uri .= '?' . $pstring;