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 (now a plugin)
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;
129 var $since_id = null;
131 var $callback = null;
134 var $access = self::READ_ONLY; // read (default) or read-write
136 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
141 * @param array $args Web and URL arguments
143 * @return boolean false if user doesn't exist
145 protected function prepare(array $args=array())
147 GNUsocial::setApi(true); // reduce exception reports to aid in debugging
148 parent::prepare($args);
150 $this->format = $this->arg('format');
151 $this->callback = $this->arg('callback');
152 $this->page = (int)$this->arg('page', 1);
153 $this->count = (int)$this->arg('count', 20);
154 $this->max_id = (int)$this->arg('max_id', 0);
155 $this->since_id = (int)$this->arg('since_id', 0);
157 // These two are not used everywhere, mainly just AtompubAction extensions
158 $this->offset = ($this->page-1) * $this->count;
159 $this->limit = $this->count + 1;
161 if ($this->arg('since')) {
162 header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
165 $this->source = $this->trimmed('source');
167 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
168 $this->source = 'api';
177 * @param array $args Arguments from $_REQUEST
181 protected function handle()
183 header('Access-Control-Allow-Origin: *');
188 * Overrides XMLOutputter::element to write booleans as strings (true|false).
189 * See that method's documentation for more info.
191 * @param string $tag Element type or tagname
192 * @param array $attrs Array of element attributes, as
194 * @param string $content string content of the element
198 function element($tag, $attrs=null, $content=null)
200 if (is_bool($content)) {
201 $content = ($content ? 'true' : 'false');
204 return parent::element($tag, $attrs, $content);
207 function twitterUserArray($profile, $get_notice=false)
209 $twitter_user = array();
212 $user = $profile->getUser();
213 } catch (NoSuchUserException $e) {
217 $twitter_user['id'] = intval($profile->id);
218 $twitter_user['name'] = $profile->getBestName();
219 $twitter_user['screen_name'] = $profile->nickname;
220 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
221 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
223 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
224 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
225 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
227 // START introduced by qvitter API, not necessary for StatusNet API
228 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
230 $avatar = Avatar::getUploaded($profile);
231 $origurl = $avatar->displayUrl();
232 } catch (Exception $e) {
233 $origurl = $twitter_user['profile_image_url_profile_size'];
235 $twitter_user['profile_image_url_original'] = $origurl;
237 $twitter_user['groups_count'] = $profile->getGroupCount();
238 foreach (array('linkcolor', 'backgroundcolor') as $key) {
239 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
241 // END introduced by qvitter API, not necessary for StatusNet API
243 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
244 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
245 $twitter_user['followers_count'] = $profile->subscriberCount();
247 // Note: some profiles don't have an associated user
249 $twitter_user['friends_count'] = $profile->subscriptionCount();
251 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
255 if (!empty($user) && $user->timezone) {
256 $timezone = $user->timezone;
260 $t->setTimezone(new DateTimeZone($timezone));
262 $twitter_user['utc_offset'] = $t->format('Z');
263 $twitter_user['time_zone'] = $timezone;
264 $twitter_user['statuses_count'] = $profile->noticeCount();
266 // Is the requesting user following this user?
267 // These values might actually also mean "unknown". Ambiguity issues?
268 $twitter_user['following'] = false;
269 $twitter_user['statusnet_blocking'] = false;
270 $twitter_user['notifications'] = false;
272 if ($this->scoped instanceof Profile) {
274 $sub = Subscription::getSubscription($this->scoped, $profile);
276 $twitter_user['following'] = true;
277 $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
278 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
279 } catch (NoResultException $e) {
280 // well, the values are already false...
285 $notice = $profile->getCurrentNotice();
286 if ($notice instanceof Notice) {
288 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
292 // StatusNet-specific
294 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
296 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
297 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
299 return $twitter_user;
302 function twitterStatusArray($notice, $include_user=true)
304 $base = $this->twitterSimpleStatusArray($notice, $include_user);
306 // FIXME: MOVE TO SHARE PLUGIN
307 if (!empty($notice->repeat_of)) {
308 $original = Notice::getKV('id', $notice->repeat_of);
309 if ($original instanceof Notice) {
310 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
311 $base['retweeted_status'] = $orig_array;
318 function twitterSimpleStatusArray($notice, $include_user=true)
320 $profile = $notice->getProfile();
322 $twitter_status = array();
323 $twitter_status['text'] = $notice->content;
324 $twitter_status['truncated'] = false; # Not possible on StatusNet
325 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
327 // We could just do $notice->reply_to but maybe the future holds a
328 // different story for parenting.
329 $parent = $notice->getParent();
330 $in_reply_to = $parent->id;
331 } catch (NoParentNoticeException $e) {
334 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
338 $ns = $notice->getSource();
339 if ($ns instanceof Notice_source) {
340 if (!empty($ns->name) && !empty($ns->url)) {
341 $source = '<a href="'
342 . htmlspecialchars($ns->url)
343 . '" rel="nofollow">'
344 . htmlspecialchars($ns->name)
351 $twitter_status['uri'] = $notice->getUri();
352 $twitter_status['source'] = $source;
353 $twitter_status['id'] = intval($notice->id);
355 $replier_profile = null;
357 if ($notice->reply_to) {
358 $reply = Notice::getKV(intval($notice->reply_to));
360 $replier_profile = $reply->getProfile();
364 $twitter_status['in_reply_to_user_id'] =
365 ($replier_profile) ? intval($replier_profile->id) : null;
366 $twitter_status['in_reply_to_screen_name'] =
367 ($replier_profile) ? $replier_profile->nickname : null;
369 if (isset($notice->lat) && isset($notice->lon)) {
370 // This is the format that GeoJSON expects stuff to be in
371 $twitter_status['geo'] = array('type' => 'Point',
372 'coordinates' => array((float) $notice->lat,
373 (float) $notice->lon));
375 $twitter_status['geo'] = null;
379 $attachments = $notice->attachments();
381 if (!empty($attachments)) {
383 $twitter_status['attachments'] = array();
385 foreach ($attachments as $attachment) {
387 $enclosure_o = $attachment->getEnclosure();
388 $enclosure = array();
389 $enclosure['url'] = $enclosure_o->url;
390 $enclosure['mimetype'] = $enclosure_o->mimetype;
391 $enclosure['size'] = $enclosure_o->size;
392 $twitter_status['attachments'][] = $enclosure;
393 } catch (ServerException $e) {
394 // There was not enough metadata available
399 if ($include_user && $profile) {
400 // Don't get notice (recursive!)
401 $twitter_user = $this->twitterUserArray($profile, false);
402 $twitter_status['user'] = $twitter_user;
405 // StatusNet-specific
407 $twitter_status['statusnet_html'] = $notice->rendered;
408 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
410 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
411 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
412 array('include_user'=>$include_user)));
414 return $twitter_status;
417 function twitterGroupArray($group)
419 $twitter_group = array();
421 $twitter_group['id'] = intval($group->id);
422 $twitter_group['url'] = $group->permalink();
423 $twitter_group['nickname'] = $group->nickname;
424 $twitter_group['fullname'] = $group->fullname;
426 if ($this->scoped instanceof Profile) {
427 $twitter_group['member'] = $this->scoped->isMember($group);
428 $twitter_group['blocked'] = Group_block::isBlocked(
434 $twitter_group['admin_count'] = $group->getAdminCount();
435 $twitter_group['member_count'] = $group->getMemberCount();
436 $twitter_group['original_logo'] = $group->original_logo;
437 $twitter_group['homepage_logo'] = $group->homepage_logo;
438 $twitter_group['stream_logo'] = $group->stream_logo;
439 $twitter_group['mini_logo'] = $group->mini_logo;
440 $twitter_group['homepage'] = $group->homepage;
441 $twitter_group['description'] = $group->description;
442 $twitter_group['location'] = $group->location;
443 $twitter_group['created'] = $this->dateTwitter($group->created);
444 $twitter_group['modified'] = $this->dateTwitter($group->modified);
446 return $twitter_group;
449 function twitterRssGroupArray($group)
452 $entry['content']=$group->description;
453 $entry['title']=$group->nickname;
454 $entry['link']=$group->permalink();
455 $entry['published']=common_date_iso8601($group->created);
456 $entry['updated']==common_date_iso8601($group->modified);
457 $taguribase = common_config('integration', 'groupuri');
458 $entry['id'] = "group:$groupuribase:$entry[link]";
460 $entry['description'] = $entry['content'];
461 $entry['pubDate'] = common_date_rfc2822($group->created);
462 $entry['guid'] = $entry['link'];
467 function twitterListArray($list)
469 $profile = Profile::getKV('id', $list->tagger);
471 $twitter_list = array();
472 $twitter_list['id'] = $list->id;
473 $twitter_list['name'] = $list->tag;
474 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
475 $twitter_list['slug'] = $list->tag;
476 $twitter_list['description'] = $list->description;
477 $twitter_list['subscriber_count'] = $list->subscriberCount();
478 $twitter_list['member_count'] = $list->taggedCount();
479 $twitter_list['uri'] = $list->getUri();
481 if ($this->scoped instanceof Profile) {
482 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
484 $twitter_list['following'] = false;
487 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
488 $twitter_list['user'] = $this->twitterUserArray($profile, false);
490 return $twitter_list;
493 function twitterRssEntryArray($notice)
497 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
498 $profile = $notice->getProfile();
500 // We trim() to avoid extraneous whitespace in the output
502 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
503 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
504 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
505 $entry['published'] = common_date_iso8601($notice->created);
507 $taguribase = TagURI::base();
508 $entry['id'] = "tag:$taguribase:$entry[link]";
510 $entry['updated'] = $entry['published'];
511 $entry['author'] = $profile->getBestName();
514 $attachments = $notice->attachments();
515 $enclosures = array();
517 foreach ($attachments as $attachment) {
519 $enclosure_o = $attachment->getEnclosure();
520 $enclosure = array();
521 $enclosure['url'] = $enclosure_o->url;
522 $enclosure['mimetype'] = $enclosure_o->mimetype;
523 $enclosure['size'] = $enclosure_o->size;
524 $enclosures[] = $enclosure;
525 } catch (ServerException $e) {
526 // There was not enough metadata available
530 if (!empty($enclosures)) {
531 $entry['enclosures'] = $enclosures;
535 $tag = new Notice_tag();
536 $tag->notice_id = $notice->id;
538 $entry['tags']=array();
539 while ($tag->fetch()) {
540 $entry['tags'][]=$tag->tag;
546 $entry['description'] = $entry['content'];
547 $entry['pubDate'] = common_date_rfc2822($notice->created);
548 $entry['guid'] = $entry['link'];
550 if (isset($notice->lat) && isset($notice->lon)) {
551 // This is the format that GeoJSON expects stuff to be in.
552 // showGeoRSS() below uses it for XML output, so we reuse it
553 $entry['geo'] = array('type' => 'Point',
554 'coordinates' => array((float) $notice->lat,
555 (float) $notice->lon));
557 $entry['geo'] = null;
560 Event::handle('EndRssEntryArray', array($notice, &$entry));
566 function twitterRelationshipArray($source, $target)
568 $relationship = array();
570 $relationship['source'] =
571 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
572 $relationship['target'] =
573 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
575 return array('relationship' => $relationship);
578 function relationshipDetailsArray(Profile $source, Profile $target)
582 $details['screen_name'] = $source->getNickname();
583 $details['followed_by'] = $target->isSubscribed($source);
586 $sub = Subscription::getSubscription($source, $target);
587 $details['following'] = true;
588 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
589 } catch (NoResultException $e) {
590 $details['following'] = false;
591 $details['notifications_enabled'] = false;
594 $details['blocking'] = $source->hasBlocked($target);
595 $details['id'] = intval($source->id);
600 function showTwitterXmlRelationship($relationship)
602 $this->elementStart('relationship');
604 foreach($relationship as $element => $value) {
605 if ($element == 'source' || $element == 'target') {
606 $this->elementStart($element);
607 $this->showXmlRelationshipDetails($value);
608 $this->elementEnd($element);
612 $this->elementEnd('relationship');
615 function showXmlRelationshipDetails($details)
617 foreach($details as $element => $value) {
618 $this->element($element, null, $value);
622 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
626 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
628 $this->elementStart($tag, $attrs);
629 foreach($twitter_status as $element => $value) {
632 $this->showTwitterXmlUser($twitter_status['user']);
635 $this->element($element, null, common_xml_safe_str($value));
638 $this->showXmlAttachments($twitter_status['attachments']);
641 $this->showGeoXML($value);
643 case 'retweeted_status':
644 // FIXME: MOVE TO SHARE PLUGIN
645 $this->showTwitterXmlStatus($value, 'retweeted_status');
648 if (strncmp($element, 'statusnet_', 10) == 0) {
649 $this->element('statusnet:'.substr($element, 10), null, $value);
651 $this->element($element, null, $value);
655 $this->elementEnd($tag);
658 function showTwitterXmlGroup($twitter_group)
660 $this->elementStart('group');
661 foreach($twitter_group as $element => $value) {
662 $this->element($element, null, $value);
664 $this->elementEnd('group');
667 function showTwitterXmlList($twitter_list)
669 $this->elementStart('list');
670 foreach($twitter_list as $element => $value) {
671 if($element == 'user') {
672 $this->showTwitterXmlUser($value, 'user');
675 $this->element($element, null, $value);
678 $this->elementEnd('list');
681 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
685 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
687 $this->elementStart($role, $attrs);
688 foreach($twitter_user as $element => $value) {
689 if ($element == 'status') {
690 $this->showTwitterXmlStatus($twitter_user['status']);
691 } else if (strncmp($element, 'statusnet_', 10) == 0) {
692 $this->element('statusnet:'.substr($element, 10), null, $value);
694 $this->element($element, null, $value);
697 $this->elementEnd($role);
700 function showXmlAttachments($attachments) {
701 if (!empty($attachments)) {
702 $this->elementStart('attachments', array('type' => 'array'));
703 foreach ($attachments as $attachment) {
705 $attrs['url'] = $attachment['url'];
706 $attrs['mimetype'] = $attachment['mimetype'];
707 $attrs['size'] = $attachment['size'];
708 $this->element('enclosure', $attrs, '');
710 $this->elementEnd('attachments');
714 function showGeoXML($geo)
718 $this->element('geo');
720 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
721 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
722 $this->elementEnd('geo');
726 function showGeoRSS($geo)
732 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
737 function showTwitterRssItem($entry)
739 $this->elementStart('item');
740 $this->element('title', null, $entry['title']);
741 $this->element('description', null, $entry['description']);
742 $this->element('pubDate', null, $entry['pubDate']);
743 $this->element('guid', null, $entry['guid']);
744 $this->element('link', null, $entry['link']);
746 // RSS only supports 1 enclosure per item
747 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
748 $enclosure = $entry['enclosures'][0];
749 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
752 if(array_key_exists('tags', $entry)){
753 foreach($entry['tags'] as $tag){
754 $this->element('category', null,$tag);
758 $this->showGeoRSS($entry['geo']);
759 $this->elementEnd('item');
762 function showJsonObjects($objects)
764 print(json_encode($objects));
767 function showSingleXmlStatus($notice)
769 $this->initDocument('xml');
770 $twitter_status = $this->twitterStatusArray($notice);
771 $this->showTwitterXmlStatus($twitter_status, 'status', true);
772 $this->endDocument('xml');
775 function showSingleAtomStatus($notice)
777 header('Content-Type: application/atom+xml; charset=utf-8');
778 print $notice->asAtomEntry(true, true, true, $this->scoped);
781 function show_single_json_status($notice)
783 $this->initDocument('json');
784 $status = $this->twitterStatusArray($notice);
785 $this->showJsonObjects($status);
786 $this->endDocument('json');
789 function showXmlTimeline($notice)
791 $this->initDocument('xml');
792 $this->elementStart('statuses', array('type' => 'array',
793 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
795 if (is_array($notice)) {
796 $notice = new ArrayWrapper($notice);
799 while ($notice->fetch()) {
801 $twitter_status = $this->twitterStatusArray($notice);
802 $this->showTwitterXmlStatus($twitter_status);
803 } catch (Exception $e) {
804 common_log(LOG_ERR, $e->getMessage());
809 $this->elementEnd('statuses');
810 $this->endDocument('xml');
813 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
815 $this->initDocument('rss');
817 $this->element('title', null, $title);
818 $this->element('link', null, $link);
820 if (!is_null($self)) {
824 'type' => 'application/rss+xml',
831 if (!is_null($suplink)) {
832 // For FriendFeed's SUP protocol
833 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
834 'rel' => 'http://api.friendfeed.com/2008/03#sup',
836 'type' => 'application/json'));
839 if (!is_null($logo)) {
840 $this->elementStart('image');
841 $this->element('link', null, $link);
842 $this->element('title', null, $title);
843 $this->element('url', null, $logo);
844 $this->elementEnd('image');
847 $this->element('description', null, $subtitle);
848 $this->element('language', null, 'en-us');
849 $this->element('ttl', null, '40');
851 if (is_array($notice)) {
852 $notice = new ArrayWrapper($notice);
855 while ($notice->fetch()) {
857 $entry = $this->twitterRssEntryArray($notice);
858 $this->showTwitterRssItem($entry);
859 } catch (Exception $e) {
860 common_log(LOG_ERR, $e->getMessage());
861 // continue on exceptions
865 $this->endTwitterRss();
868 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
870 $this->initDocument('atom');
872 $this->element('title', null, $title);
873 $this->element('id', null, $id);
874 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
876 if (!is_null($logo)) {
877 $this->element('logo',null,$logo);
880 if (!is_null($suplink)) {
881 // For FriendFeed's SUP protocol
882 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
884 'type' => 'application/json'));
887 if (!is_null($selfuri)) {
888 $this->element('link', array('href' => $selfuri,
889 'rel' => 'self', 'type' => 'application/atom+xml'), null);
892 $this->element('updated', null, common_date_iso8601('now'));
893 $this->element('subtitle', null, $subtitle);
895 if (is_array($notice)) {
896 $notice = new ArrayWrapper($notice);
899 while ($notice->fetch()) {
901 $this->raw($notice->asAtomEntry());
902 } catch (Exception $e) {
903 common_log(LOG_ERR, $e->getMessage());
908 $this->endDocument('atom');
911 function showRssGroups($group, $title, $link, $subtitle)
913 $this->initDocument('rss');
915 $this->element('title', null, $title);
916 $this->element('link', null, $link);
917 $this->element('description', null, $subtitle);
918 $this->element('language', null, 'en-us');
919 $this->element('ttl', null, '40');
921 if (is_array($group)) {
922 foreach ($group as $g) {
923 $twitter_group = $this->twitterRssGroupArray($g);
924 $this->showTwitterRssItem($twitter_group);
927 while ($group->fetch()) {
928 $twitter_group = $this->twitterRssGroupArray($group);
929 $this->showTwitterRssItem($twitter_group);
933 $this->endTwitterRss();
936 function showTwitterAtomEntry($entry)
938 $this->elementStart('entry');
939 $this->element('title', null, common_xml_safe_str($entry['title']));
942 array('type' => 'html'),
943 common_xml_safe_str($entry['content'])
945 $this->element('id', null, $entry['id']);
946 $this->element('published', null, $entry['published']);
947 $this->element('updated', null, $entry['updated']);
948 $this->element('link', array('type' => 'text/html',
949 'href' => $entry['link'],
950 'rel' => 'alternate'));
951 $this->element('link', array('type' => $entry['avatar-type'],
952 'href' => $entry['avatar'],
954 $this->elementStart('author');
956 $this->element('name', null, $entry['author-name']);
957 $this->element('uri', null, $entry['author-uri']);
959 $this->elementEnd('author');
960 $this->elementEnd('entry');
963 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
965 $this->initDocument('atom');
967 $this->element('title', null, common_xml_safe_str($title));
968 $this->element('id', null, $id);
969 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
971 if (!is_null($selfuri)) {
972 $this->element('link', array('href' => $selfuri,
973 'rel' => 'self', 'type' => 'application/atom+xml'), null);
976 $this->element('updated', null, common_date_iso8601('now'));
977 $this->element('subtitle', null, common_xml_safe_str($subtitle));
979 if (is_array($group)) {
980 foreach ($group as $g) {
981 $this->raw($g->asAtomEntry());
984 while ($group->fetch()) {
985 $this->raw($group->asAtomEntry());
989 $this->endDocument('atom');
993 function showJsonTimeline($notice)
995 $this->initDocument('json');
999 if (is_array($notice)) {
1000 $notice = new ArrayWrapper($notice);
1003 while ($notice->fetch()) {
1005 $twitter_status = $this->twitterStatusArray($notice);
1006 array_push($statuses, $twitter_status);
1007 } catch (Exception $e) {
1008 common_log(LOG_ERR, $e->getMessage());
1013 $this->showJsonObjects($statuses);
1015 $this->endDocument('json');
1018 function showJsonGroups($group)
1020 $this->initDocument('json');
1024 if (is_array($group)) {
1025 foreach ($group as $g) {
1026 $twitter_group = $this->twitterGroupArray($g);
1027 array_push($groups, $twitter_group);
1030 while ($group->fetch()) {
1031 $twitter_group = $this->twitterGroupArray($group);
1032 array_push($groups, $twitter_group);
1036 $this->showJsonObjects($groups);
1038 $this->endDocument('json');
1041 function showXmlGroups($group)
1044 $this->initDocument('xml');
1045 $this->elementStart('groups', array('type' => 'array'));
1047 if (is_array($group)) {
1048 foreach ($group as $g) {
1049 $twitter_group = $this->twitterGroupArray($g);
1050 $this->showTwitterXmlGroup($twitter_group);
1053 while ($group->fetch()) {
1054 $twitter_group = $this->twitterGroupArray($group);
1055 $this->showTwitterXmlGroup($twitter_group);
1059 $this->elementEnd('groups');
1060 $this->endDocument('xml');
1063 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1066 $this->initDocument('xml');
1067 $this->elementStart('lists_list');
1068 $this->elementStart('lists', array('type' => 'array'));
1070 if (is_array($list)) {
1071 foreach ($list as $l) {
1072 $twitter_list = $this->twitterListArray($l);
1073 $this->showTwitterXmlList($twitter_list);
1076 while ($list->fetch()) {
1077 $twitter_list = $this->twitterListArray($list);
1078 $this->showTwitterXmlList($twitter_list);
1082 $this->elementEnd('lists');
1084 $this->element('next_cursor', null, $next_cursor);
1085 $this->element('previous_cursor', null, $prev_cursor);
1087 $this->elementEnd('lists_list');
1088 $this->endDocument('xml');
1091 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1093 $this->initDocument('json');
1097 if (is_array($list)) {
1098 foreach ($list as $l) {
1099 $twitter_list = $this->twitterListArray($l);
1100 array_push($lists, $twitter_list);
1103 while ($list->fetch()) {
1104 $twitter_list = $this->twitterListArray($list);
1105 array_push($lists, $twitter_list);
1109 $lists_list = array(
1111 'next_cursor' => $next_cursor,
1112 'next_cursor_str' => strval($next_cursor),
1113 'previous_cursor' => $prev_cursor,
1114 'previous_cursor_str' => strval($prev_cursor)
1117 $this->showJsonObjects($lists_list);
1119 $this->endDocument('json');
1122 function showTwitterXmlUsers($user)
1124 $this->initDocument('xml');
1125 $this->elementStart('users', array('type' => 'array',
1126 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1128 if (is_array($user)) {
1129 foreach ($user as $u) {
1130 $twitter_user = $this->twitterUserArray($u);
1131 $this->showTwitterXmlUser($twitter_user);
1134 while ($user->fetch()) {
1135 $twitter_user = $this->twitterUserArray($user);
1136 $this->showTwitterXmlUser($twitter_user);
1140 $this->elementEnd('users');
1141 $this->endDocument('xml');
1144 function showJsonUsers($user)
1146 $this->initDocument('json');
1150 if (is_array($user)) {
1151 foreach ($user as $u) {
1152 $twitter_user = $this->twitterUserArray($u);
1153 array_push($users, $twitter_user);
1156 while ($user->fetch()) {
1157 $twitter_user = $this->twitterUserArray($user);
1158 array_push($users, $twitter_user);
1162 $this->showJsonObjects($users);
1164 $this->endDocument('json');
1167 function showSingleJsonGroup($group)
1169 $this->initDocument('json');
1170 $twitter_group = $this->twitterGroupArray($group);
1171 $this->showJsonObjects($twitter_group);
1172 $this->endDocument('json');
1175 function showSingleXmlGroup($group)
1177 $this->initDocument('xml');
1178 $twitter_group = $this->twitterGroupArray($group);
1179 $this->showTwitterXmlGroup($twitter_group);
1180 $this->endDocument('xml');
1183 function showSingleJsonList($list)
1185 $this->initDocument('json');
1186 $twitter_list = $this->twitterListArray($list);
1187 $this->showJsonObjects($twitter_list);
1188 $this->endDocument('json');
1191 function showSingleXmlList($list)
1193 $this->initDocument('xml');
1194 $twitter_list = $this->twitterListArray($list);
1195 $this->showTwitterXmlList($twitter_list);
1196 $this->endDocument('xml');
1199 function dateTwitter($dt)
1201 $dateStr = date('d F Y H:i:s', strtotime($dt));
1202 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1203 $d->setTimezone(new DateTimeZone(common_timezone()));
1204 return $d->format('D M d H:i:s O Y');
1207 function initDocument($type='xml')
1211 header('Content-Type: application/xml; charset=utf-8');
1215 header('Content-Type: application/json; charset=utf-8');
1217 // Check for JSONP callback
1218 if (isset($this->callback)) {
1219 print $this->callback . '(';
1223 header("Content-Type: application/rss+xml; charset=utf-8");
1224 $this->initTwitterRss();
1227 header('Content-Type: application/atom+xml; charset=utf-8');
1228 $this->initTwitterAtom();
1231 // TRANS: Client error on an API request with an unsupported data format.
1232 $this->clientError(_('Not a supported data format.'));
1238 function endDocument($type='xml')
1245 // Check for JSONP callback
1246 if (isset($this->callback)) {
1251 $this->endTwitterRss();
1254 $this->endTwitterRss();
1257 // TRANS: Client error on an API request with an unsupported data format.
1258 $this->clientError(_('Not a supported data format.'));
1263 function initTwitterRss()
1266 $this->elementStart(
1270 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1271 'xmlns:georss' => 'http://www.georss.org/georss'
1274 $this->elementStart('channel');
1275 Event::handle('StartApiRss', array($this));
1278 function endTwitterRss()
1280 $this->elementEnd('channel');
1281 $this->elementEnd('rss');
1285 function initTwitterAtom()
1288 // FIXME: don't hardcode the language here!
1289 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1290 'xml:lang' => 'en-US',
1291 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1294 function endTwitterAtom()
1296 $this->elementEnd('feed');
1300 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1302 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1303 switch ($content_type) {
1305 $this->showTwitterXmlUser($profile_array);
1308 $this->showJsonObjects($profile_array);
1311 // TRANS: Client error on an API request with an unsupported data format.
1312 $this->clientError(_('Not a supported data format.'));
1317 private static function is_decimal($str)
1319 return preg_match('/^[0-9]+$/', $str);
1322 function getTargetUser($id)
1325 // Twitter supports these other ways of passing the user ID
1326 if (self::is_decimal($this->arg('id'))) {
1327 return User::getKV($this->arg('id'));
1328 } else if ($this->arg('id')) {
1329 $nickname = common_canonical_nickname($this->arg('id'));
1330 return User::getKV('nickname', $nickname);
1331 } else if ($this->arg('user_id')) {
1332 // This is to ensure that a non-numeric user_id still
1333 // overrides screen_name even if it doesn't get used
1334 if (self::is_decimal($this->arg('user_id'))) {
1335 return User::getKV('id', $this->arg('user_id'));
1337 } else if ($this->arg('screen_name')) {
1338 $nickname = common_canonical_nickname($this->arg('screen_name'));
1339 return User::getKV('nickname', $nickname);
1341 // Fall back to trying the currently authenticated user
1342 return $this->scoped->getUser();
1345 } else if (self::is_decimal($id)) {
1346 return User::getKV($id);
1348 $nickname = common_canonical_nickname($id);
1349 return User::getKV('nickname', $nickname);
1353 function getTargetProfile($id)
1357 // Twitter supports these other ways of passing the user ID
1358 if (self::is_decimal($this->arg('id'))) {
1359 return Profile::getKV($this->arg('id'));
1360 } else if ($this->arg('id')) {
1361 // Screen names currently can only uniquely identify a local user.
1362 $nickname = common_canonical_nickname($this->arg('id'));
1363 $user = User::getKV('nickname', $nickname);
1364 return $user ? $user->getProfile() : null;
1365 } else if ($this->arg('user_id')) {
1366 // This is to ensure that a non-numeric user_id still
1367 // overrides screen_name even if it doesn't get used
1368 if (self::is_decimal($this->arg('user_id'))) {
1369 return Profile::getKV('id', $this->arg('user_id'));
1371 } else if ($this->arg('screen_name')) {
1372 $nickname = common_canonical_nickname($this->arg('screen_name'));
1373 $user = User::getKV('nickname', $nickname);
1374 return $user instanceof User ? $user->getProfile() : null;
1376 // Fall back to trying the currently authenticated user
1377 return $this->scoped;
1379 } else if (self::is_decimal($id)) {
1380 return Profile::getKV($id);
1382 $nickname = common_canonical_nickname($id);
1383 $user = User::getKV('nickname', $nickname);
1384 return $user ? $user->getProfile() : null;
1388 function getTargetGroup($id)
1391 if (self::is_decimal($this->arg('id'))) {
1392 return User_group::getKV('id', $this->arg('id'));
1393 } else if ($this->arg('id')) {
1394 return User_group::getForNickname($this->arg('id'));
1395 } else if ($this->arg('group_id')) {
1396 // This is to ensure that a non-numeric group_id still
1397 // overrides group_name even if it doesn't get used
1398 if (self::is_decimal($this->arg('group_id'))) {
1399 return User_group::getKV('id', $this->arg('group_id'));
1401 } else if ($this->arg('group_name')) {
1402 return User_group::getForNickname($this->arg('group_name'));
1405 } else if (self::is_decimal($id)) {
1406 return User_group::getKV('id', $id);
1407 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1408 return User_group::getKV('uri', urldecode($this->arg('uri')));
1410 return User_group::getForNickname($id);
1414 function getTargetList($user=null, $id=null)
1416 $tagger = $this->getTargetUser($user);
1420 $id = $this->arg('id');
1424 if (is_numeric($id)) {
1425 $list = Profile_list::getKV('id', $id);
1427 // only if the list with the id belongs to the tagger
1428 if(empty($list) || $list->tagger != $tagger->id) {
1433 $tag = common_canonical_tag($id);
1434 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1437 if (!empty($list) && $list->private) {
1438 if ($this->scoped->id == $list->tagger) {
1449 * Returns query argument or default value if not found. Certain
1450 * parameters used throughout the API are lightly scrubbed and
1451 * bounds checked. This overrides Action::arg().
1453 * @param string $key requested argument
1454 * @param string $def default value to return if $key is not provided
1458 function arg($key, $def=null)
1460 // XXX: Do even more input validation/scrubbing?
1462 if (array_key_exists($key, $this->args)) {
1465 $page = (int)$this->args['page'];
1466 return ($page < 1) ? 1 : $page;
1468 $count = (int)$this->args['count'];
1471 } elseif ($count > 200) {
1477 $since_id = (int)$this->args['since_id'];
1478 return ($since_id < 1) ? 0 : $since_id;
1480 $max_id = (int)$this->args['max_id'];
1481 return ($max_id < 1) ? 0 : $max_id;
1483 return parent::arg($key, $def);
1491 * Calculate the complete URI that called up this action. Used for
1492 * Atom rel="self" links. Warning: this is funky.
1494 * @return string URL a URL suitable for rel="self" Atom links
1496 function getSelfUri()
1498 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1500 $id = $this->arg('id');
1501 $aargs = array('format' => $this->format);
1506 $user = $this->arg('user');
1507 if (!empty($user)) {
1508 $aargs['user'] = $user;
1511 $tag = $this->arg('tag');
1513 $aargs['tag'] = $tag;
1516 parse_str($_SERVER['QUERY_STRING'], $params);
1518 if (!empty($params)) {
1519 unset($params['p']);
1520 $pstring = http_build_query($params);
1523 $uri = common_local_url($action, $aargs);
1525 if (!empty($pstring)) {
1526 $uri .= '?' . $pstring;