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 StatusNet::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-StatusNet-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 if (!empty($notice->repeat_of)) {
307 $original = Notice::getKV('id', $notice->repeat_of);
308 if ($original instanceof Notice) {
309 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
310 $base['retweeted_status'] = $orig_array;
317 function twitterSimpleStatusArray($notice, $include_user=true)
319 $profile = $notice->getProfile();
321 $twitter_status = array();
322 $twitter_status['text'] = $notice->content;
323 $twitter_status['truncated'] = false; # Not possible on StatusNet
324 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
326 // We could just do $notice->reply_to but maybe the future holds a
327 // different story for parenting.
328 $parent = $notice->getParent();
329 $in_reply_to = $parent->id;
330 } catch (Exception $e) {
333 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
337 $ns = $notice->getSource();
338 if ($ns instanceof Notice_source) {
339 if (!empty($ns->name) && !empty($ns->url)) {
340 $source = '<a href="'
341 . htmlspecialchars($ns->url)
342 . '" rel="nofollow">'
343 . htmlspecialchars($ns->name)
350 $twitter_status['uri'] = $notice->getUri();
351 $twitter_status['source'] = $source;
352 $twitter_status['id'] = intval($notice->id);
354 $replier_profile = null;
356 if ($notice->reply_to) {
357 $reply = Notice::getKV(intval($notice->reply_to));
359 $replier_profile = $reply->getProfile();
363 $twitter_status['in_reply_to_user_id'] =
364 ($replier_profile) ? intval($replier_profile->id) : null;
365 $twitter_status['in_reply_to_screen_name'] =
366 ($replier_profile) ? $replier_profile->nickname : null;
368 if (isset($notice->lat) && isset($notice->lon)) {
369 // This is the format that GeoJSON expects stuff to be in
370 $twitter_status['geo'] = array('type' => 'Point',
371 'coordinates' => array((float) $notice->lat,
372 (float) $notice->lon));
374 $twitter_status['geo'] = null;
377 if (!is_null($this->scoped)) {
378 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
380 $twitter_status['repeated'] = false;
384 $attachments = $notice->attachments();
386 if (!empty($attachments)) {
388 $twitter_status['attachments'] = array();
390 foreach ($attachments as $attachment) {
392 $enclosure_o = $attachment->getEnclosure();
393 $enclosure = array();
394 $enclosure['url'] = $enclosure_o->url;
395 $enclosure['mimetype'] = $enclosure_o->mimetype;
396 $enclosure['size'] = $enclosure_o->size;
397 $twitter_status['attachments'][] = $enclosure;
398 } catch (ServerException $e) {
399 // There was not enough metadata available
404 if ($include_user && $profile) {
405 // Don't get notice (recursive!)
406 $twitter_user = $this->twitterUserArray($profile, false);
407 $twitter_status['user'] = $twitter_user;
410 // StatusNet-specific
412 $twitter_status['statusnet_html'] = $notice->rendered;
413 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
415 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
416 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
417 array('include_user'=>$include_user)));
419 return $twitter_status;
422 function twitterGroupArray($group)
424 $twitter_group = array();
426 $twitter_group['id'] = intval($group->id);
427 $twitter_group['url'] = $group->permalink();
428 $twitter_group['nickname'] = $group->nickname;
429 $twitter_group['fullname'] = $group->fullname;
431 if ($this->scoped instanceof Profile) {
432 $twitter_group['member'] = $this->scoped->isMember($group);
433 $twitter_group['blocked'] = Group_block::isBlocked(
439 $twitter_group['admin_count'] = $group->getAdminCount();
440 $twitter_group['member_count'] = $group->getMemberCount();
441 $twitter_group['original_logo'] = $group->original_logo;
442 $twitter_group['homepage_logo'] = $group->homepage_logo;
443 $twitter_group['stream_logo'] = $group->stream_logo;
444 $twitter_group['mini_logo'] = $group->mini_logo;
445 $twitter_group['homepage'] = $group->homepage;
446 $twitter_group['description'] = $group->description;
447 $twitter_group['location'] = $group->location;
448 $twitter_group['created'] = $this->dateTwitter($group->created);
449 $twitter_group['modified'] = $this->dateTwitter($group->modified);
451 return $twitter_group;
454 function twitterRssGroupArray($group)
457 $entry['content']=$group->description;
458 $entry['title']=$group->nickname;
459 $entry['link']=$group->permalink();
460 $entry['published']=common_date_iso8601($group->created);
461 $entry['updated']==common_date_iso8601($group->modified);
462 $taguribase = common_config('integration', 'groupuri');
463 $entry['id'] = "group:$groupuribase:$entry[link]";
465 $entry['description'] = $entry['content'];
466 $entry['pubDate'] = common_date_rfc2822($group->created);
467 $entry['guid'] = $entry['link'];
472 function twitterListArray($list)
474 $profile = Profile::getKV('id', $list->tagger);
476 $twitter_list = array();
477 $twitter_list['id'] = $list->id;
478 $twitter_list['name'] = $list->tag;
479 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
480 $twitter_list['slug'] = $list->tag;
481 $twitter_list['description'] = $list->description;
482 $twitter_list['subscriber_count'] = $list->subscriberCount();
483 $twitter_list['member_count'] = $list->taggedCount();
484 $twitter_list['uri'] = $list->getUri();
486 if ($this->scoped instanceof Profile) {
487 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
489 $twitter_list['following'] = false;
492 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
493 $twitter_list['user'] = $this->twitterUserArray($profile, false);
495 return $twitter_list;
498 function twitterRssEntryArray($notice)
502 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
503 $profile = $notice->getProfile();
505 // We trim() to avoid extraneous whitespace in the output
507 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
508 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
509 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
510 $entry['published'] = common_date_iso8601($notice->created);
512 $taguribase = TagURI::base();
513 $entry['id'] = "tag:$taguribase:$entry[link]";
515 $entry['updated'] = $entry['published'];
516 $entry['author'] = $profile->getBestName();
519 $attachments = $notice->attachments();
520 $enclosures = array();
522 foreach ($attachments as $attachment) {
524 $enclosure_o = $attachment->getEnclosure();
525 $enclosure = array();
526 $enclosure['url'] = $enclosure_o->url;
527 $enclosure['mimetype'] = $enclosure_o->mimetype;
528 $enclosure['size'] = $enclosure_o->size;
529 $enclosures[] = $enclosure;
530 } catch (ServerException $e) {
531 // There was not enough metadata available
535 if (!empty($enclosures)) {
536 $entry['enclosures'] = $enclosures;
540 $tag = new Notice_tag();
541 $tag->notice_id = $notice->id;
543 $entry['tags']=array();
544 while ($tag->fetch()) {
545 $entry['tags'][]=$tag->tag;
551 $entry['description'] = $entry['content'];
552 $entry['pubDate'] = common_date_rfc2822($notice->created);
553 $entry['guid'] = $entry['link'];
555 if (isset($notice->lat) && isset($notice->lon)) {
556 // This is the format that GeoJSON expects stuff to be in.
557 // showGeoRSS() below uses it for XML output, so we reuse it
558 $entry['geo'] = array('type' => 'Point',
559 'coordinates' => array((float) $notice->lat,
560 (float) $notice->lon));
562 $entry['geo'] = null;
565 Event::handle('EndRssEntryArray', array($notice, &$entry));
571 function twitterRelationshipArray($source, $target)
573 $relationship = array();
575 $relationship['source'] =
576 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
577 $relationship['target'] =
578 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
580 return array('relationship' => $relationship);
583 function relationshipDetailsArray(Profile $source, Profile $target)
587 $details['screen_name'] = $source->getNickname();
588 $details['followed_by'] = $target->isSubscribed($source);
591 $sub = Subscription::getSubscription($source, $target);
592 $details['following'] = true;
593 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
594 } catch (NoResultException $e) {
595 $details['following'] = false;
596 $details['notifications_enabled'] = false;
599 $details['blocking'] = $source->hasBlocked($target);
600 $details['id'] = intval($source->id);
605 function showTwitterXmlRelationship($relationship)
607 $this->elementStart('relationship');
609 foreach($relationship as $element => $value) {
610 if ($element == 'source' || $element == 'target') {
611 $this->elementStart($element);
612 $this->showXmlRelationshipDetails($value);
613 $this->elementEnd($element);
617 $this->elementEnd('relationship');
620 function showXmlRelationshipDetails($details)
622 foreach($details as $element => $value) {
623 $this->element($element, null, $value);
627 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
631 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
633 $this->elementStart($tag, $attrs);
634 foreach($twitter_status as $element => $value) {
637 $this->showTwitterXmlUser($twitter_status['user']);
640 $this->element($element, null, common_xml_safe_str($value));
643 $this->showXmlAttachments($twitter_status['attachments']);
646 $this->showGeoXML($value);
648 case 'retweeted_status':
649 $this->showTwitterXmlStatus($value, 'retweeted_status');
652 if (strncmp($element, 'statusnet_', 10) == 0) {
653 $this->element('statusnet:'.substr($element, 10), null, $value);
655 $this->element($element, null, $value);
659 $this->elementEnd($tag);
662 function showTwitterXmlGroup($twitter_group)
664 $this->elementStart('group');
665 foreach($twitter_group as $element => $value) {
666 $this->element($element, null, $value);
668 $this->elementEnd('group');
671 function showTwitterXmlList($twitter_list)
673 $this->elementStart('list');
674 foreach($twitter_list as $element => $value) {
675 if($element == 'user') {
676 $this->showTwitterXmlUser($value, 'user');
679 $this->element($element, null, $value);
682 $this->elementEnd('list');
685 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
689 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
691 $this->elementStart($role, $attrs);
692 foreach($twitter_user as $element => $value) {
693 if ($element == 'status') {
694 $this->showTwitterXmlStatus($twitter_user['status']);
695 } else if (strncmp($element, 'statusnet_', 10) == 0) {
696 $this->element('statusnet:'.substr($element, 10), null, $value);
698 $this->element($element, null, $value);
701 $this->elementEnd($role);
704 function showXmlAttachments($attachments) {
705 if (!empty($attachments)) {
706 $this->elementStart('attachments', array('type' => 'array'));
707 foreach ($attachments as $attachment) {
709 $attrs['url'] = $attachment['url'];
710 $attrs['mimetype'] = $attachment['mimetype'];
711 $attrs['size'] = $attachment['size'];
712 $this->element('enclosure', $attrs, '');
714 $this->elementEnd('attachments');
718 function showGeoXML($geo)
722 $this->element('geo');
724 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
725 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
726 $this->elementEnd('geo');
730 function showGeoRSS($geo)
736 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
741 function showTwitterRssItem($entry)
743 $this->elementStart('item');
744 $this->element('title', null, $entry['title']);
745 $this->element('description', null, $entry['description']);
746 $this->element('pubDate', null, $entry['pubDate']);
747 $this->element('guid', null, $entry['guid']);
748 $this->element('link', null, $entry['link']);
750 // RSS only supports 1 enclosure per item
751 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
752 $enclosure = $entry['enclosures'][0];
753 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
756 if(array_key_exists('tags', $entry)){
757 foreach($entry['tags'] as $tag){
758 $this->element('category', null,$tag);
762 $this->showGeoRSS($entry['geo']);
763 $this->elementEnd('item');
766 function showJsonObjects($objects)
768 print(json_encode($objects));
771 function showSingleXmlStatus($notice)
773 $this->initDocument('xml');
774 $twitter_status = $this->twitterStatusArray($notice);
775 $this->showTwitterXmlStatus($twitter_status, 'status', true);
776 $this->endDocument('xml');
779 function showSingleAtomStatus($notice)
781 header('Content-Type: application/atom+xml; charset=utf-8');
782 print $notice->asAtomEntry(true, true, true, $this->scoped);
785 function show_single_json_status($notice)
787 $this->initDocument('json');
788 $status = $this->twitterStatusArray($notice);
789 $this->showJsonObjects($status);
790 $this->endDocument('json');
793 function showXmlTimeline($notice)
795 $this->initDocument('xml');
796 $this->elementStart('statuses', array('type' => 'array',
797 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
799 if (is_array($notice)) {
800 $notice = new ArrayWrapper($notice);
803 while ($notice->fetch()) {
805 $twitter_status = $this->twitterStatusArray($notice);
806 $this->showTwitterXmlStatus($twitter_status);
807 } catch (Exception $e) {
808 common_log(LOG_ERR, $e->getMessage());
813 $this->elementEnd('statuses');
814 $this->endDocument('xml');
817 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
819 $this->initDocument('rss');
821 $this->element('title', null, $title);
822 $this->element('link', null, $link);
824 if (!is_null($self)) {
828 'type' => 'application/rss+xml',
835 if (!is_null($suplink)) {
836 // For FriendFeed's SUP protocol
837 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
838 'rel' => 'http://api.friendfeed.com/2008/03#sup',
840 'type' => 'application/json'));
843 if (!is_null($logo)) {
844 $this->elementStart('image');
845 $this->element('link', null, $link);
846 $this->element('title', null, $title);
847 $this->element('url', null, $logo);
848 $this->elementEnd('image');
851 $this->element('description', null, $subtitle);
852 $this->element('language', null, 'en-us');
853 $this->element('ttl', null, '40');
855 if (is_array($notice)) {
856 $notice = new ArrayWrapper($notice);
859 while ($notice->fetch()) {
861 $entry = $this->twitterRssEntryArray($notice);
862 $this->showTwitterRssItem($entry);
863 } catch (Exception $e) {
864 common_log(LOG_ERR, $e->getMessage());
865 // continue on exceptions
869 $this->endTwitterRss();
872 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
874 $this->initDocument('atom');
876 $this->element('title', null, $title);
877 $this->element('id', null, $id);
878 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
880 if (!is_null($logo)) {
881 $this->element('logo',null,$logo);
884 if (!is_null($suplink)) {
885 // For FriendFeed's SUP protocol
886 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
888 'type' => 'application/json'));
891 if (!is_null($selfuri)) {
892 $this->element('link', array('href' => $selfuri,
893 'rel' => 'self', 'type' => 'application/atom+xml'), null);
896 $this->element('updated', null, common_date_iso8601('now'));
897 $this->element('subtitle', null, $subtitle);
899 if (is_array($notice)) {
900 $notice = new ArrayWrapper($notice);
903 while ($notice->fetch()) {
905 $this->raw($notice->asAtomEntry());
906 } catch (Exception $e) {
907 common_log(LOG_ERR, $e->getMessage());
912 $this->endDocument('atom');
915 function showRssGroups($group, $title, $link, $subtitle)
917 $this->initDocument('rss');
919 $this->element('title', null, $title);
920 $this->element('link', null, $link);
921 $this->element('description', null, $subtitle);
922 $this->element('language', null, 'en-us');
923 $this->element('ttl', null, '40');
925 if (is_array($group)) {
926 foreach ($group as $g) {
927 $twitter_group = $this->twitterRssGroupArray($g);
928 $this->showTwitterRssItem($twitter_group);
931 while ($group->fetch()) {
932 $twitter_group = $this->twitterRssGroupArray($group);
933 $this->showTwitterRssItem($twitter_group);
937 $this->endTwitterRss();
940 function showTwitterAtomEntry($entry)
942 $this->elementStart('entry');
943 $this->element('title', null, common_xml_safe_str($entry['title']));
946 array('type' => 'html'),
947 common_xml_safe_str($entry['content'])
949 $this->element('id', null, $entry['id']);
950 $this->element('published', null, $entry['published']);
951 $this->element('updated', null, $entry['updated']);
952 $this->element('link', array('type' => 'text/html',
953 'href' => $entry['link'],
954 'rel' => 'alternate'));
955 $this->element('link', array('type' => $entry['avatar-type'],
956 'href' => $entry['avatar'],
958 $this->elementStart('author');
960 $this->element('name', null, $entry['author-name']);
961 $this->element('uri', null, $entry['author-uri']);
963 $this->elementEnd('author');
964 $this->elementEnd('entry');
967 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
969 $this->initDocument('atom');
971 $this->element('title', null, common_xml_safe_str($title));
972 $this->element('id', null, $id);
973 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
975 if (!is_null($selfuri)) {
976 $this->element('link', array('href' => $selfuri,
977 'rel' => 'self', 'type' => 'application/atom+xml'), null);
980 $this->element('updated', null, common_date_iso8601('now'));
981 $this->element('subtitle', null, common_xml_safe_str($subtitle));
983 if (is_array($group)) {
984 foreach ($group as $g) {
985 $this->raw($g->asAtomEntry());
988 while ($group->fetch()) {
989 $this->raw($group->asAtomEntry());
993 $this->endDocument('atom');
997 function showJsonTimeline($notice)
999 $this->initDocument('json');
1001 $statuses = array();
1003 if (is_array($notice)) {
1004 $notice = new ArrayWrapper($notice);
1007 while ($notice->fetch()) {
1009 $twitter_status = $this->twitterStatusArray($notice);
1010 array_push($statuses, $twitter_status);
1011 } catch (Exception $e) {
1012 common_log(LOG_ERR, $e->getMessage());
1017 $this->showJsonObjects($statuses);
1019 $this->endDocument('json');
1022 function showJsonGroups($group)
1024 $this->initDocument('json');
1028 if (is_array($group)) {
1029 foreach ($group as $g) {
1030 $twitter_group = $this->twitterGroupArray($g);
1031 array_push($groups, $twitter_group);
1034 while ($group->fetch()) {
1035 $twitter_group = $this->twitterGroupArray($group);
1036 array_push($groups, $twitter_group);
1040 $this->showJsonObjects($groups);
1042 $this->endDocument('json');
1045 function showXmlGroups($group)
1048 $this->initDocument('xml');
1049 $this->elementStart('groups', array('type' => 'array'));
1051 if (is_array($group)) {
1052 foreach ($group as $g) {
1053 $twitter_group = $this->twitterGroupArray($g);
1054 $this->showTwitterXmlGroup($twitter_group);
1057 while ($group->fetch()) {
1058 $twitter_group = $this->twitterGroupArray($group);
1059 $this->showTwitterXmlGroup($twitter_group);
1063 $this->elementEnd('groups');
1064 $this->endDocument('xml');
1067 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1070 $this->initDocument('xml');
1071 $this->elementStart('lists_list');
1072 $this->elementStart('lists', array('type' => 'array'));
1074 if (is_array($list)) {
1075 foreach ($list as $l) {
1076 $twitter_list = $this->twitterListArray($l);
1077 $this->showTwitterXmlList($twitter_list);
1080 while ($list->fetch()) {
1081 $twitter_list = $this->twitterListArray($list);
1082 $this->showTwitterXmlList($twitter_list);
1086 $this->elementEnd('lists');
1088 $this->element('next_cursor', null, $next_cursor);
1089 $this->element('previous_cursor', null, $prev_cursor);
1091 $this->elementEnd('lists_list');
1092 $this->endDocument('xml');
1095 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1097 $this->initDocument('json');
1101 if (is_array($list)) {
1102 foreach ($list as $l) {
1103 $twitter_list = $this->twitterListArray($l);
1104 array_push($lists, $twitter_list);
1107 while ($list->fetch()) {
1108 $twitter_list = $this->twitterListArray($list);
1109 array_push($lists, $twitter_list);
1113 $lists_list = array(
1115 'next_cursor' => $next_cursor,
1116 'next_cursor_str' => strval($next_cursor),
1117 'previous_cursor' => $prev_cursor,
1118 'previous_cursor_str' => strval($prev_cursor)
1121 $this->showJsonObjects($lists_list);
1123 $this->endDocument('json');
1126 function showTwitterXmlUsers($user)
1128 $this->initDocument('xml');
1129 $this->elementStart('users', array('type' => 'array',
1130 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1132 if (is_array($user)) {
1133 foreach ($user as $u) {
1134 $twitter_user = $this->twitterUserArray($u);
1135 $this->showTwitterXmlUser($twitter_user);
1138 while ($user->fetch()) {
1139 $twitter_user = $this->twitterUserArray($user);
1140 $this->showTwitterXmlUser($twitter_user);
1144 $this->elementEnd('users');
1145 $this->endDocument('xml');
1148 function showJsonUsers($user)
1150 $this->initDocument('json');
1154 if (is_array($user)) {
1155 foreach ($user as $u) {
1156 $twitter_user = $this->twitterUserArray($u);
1157 array_push($users, $twitter_user);
1160 while ($user->fetch()) {
1161 $twitter_user = $this->twitterUserArray($user);
1162 array_push($users, $twitter_user);
1166 $this->showJsonObjects($users);
1168 $this->endDocument('json');
1171 function showSingleJsonGroup($group)
1173 $this->initDocument('json');
1174 $twitter_group = $this->twitterGroupArray($group);
1175 $this->showJsonObjects($twitter_group);
1176 $this->endDocument('json');
1179 function showSingleXmlGroup($group)
1181 $this->initDocument('xml');
1182 $twitter_group = $this->twitterGroupArray($group);
1183 $this->showTwitterXmlGroup($twitter_group);
1184 $this->endDocument('xml');
1187 function showSingleJsonList($list)
1189 $this->initDocument('json');
1190 $twitter_list = $this->twitterListArray($list);
1191 $this->showJsonObjects($twitter_list);
1192 $this->endDocument('json');
1195 function showSingleXmlList($list)
1197 $this->initDocument('xml');
1198 $twitter_list = $this->twitterListArray($list);
1199 $this->showTwitterXmlList($twitter_list);
1200 $this->endDocument('xml');
1203 function dateTwitter($dt)
1205 $dateStr = date('d F Y H:i:s', strtotime($dt));
1206 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1207 $d->setTimezone(new DateTimeZone(common_timezone()));
1208 return $d->format('D M d H:i:s O Y');
1211 function initDocument($type='xml')
1215 header('Content-Type: application/xml; charset=utf-8');
1219 header('Content-Type: application/json; charset=utf-8');
1221 // Check for JSONP callback
1222 if (isset($this->callback)) {
1223 print $this->callback . '(';
1227 header("Content-Type: application/rss+xml; charset=utf-8");
1228 $this->initTwitterRss();
1231 header('Content-Type: application/atom+xml; charset=utf-8');
1232 $this->initTwitterAtom();
1235 // TRANS: Client error on an API request with an unsupported data format.
1236 $this->clientError(_('Not a supported data format.'));
1242 function endDocument($type='xml')
1249 // Check for JSONP callback
1250 if (isset($this->callback)) {
1255 $this->endTwitterRss();
1258 $this->endTwitterRss();
1261 // TRANS: Client error on an API request with an unsupported data format.
1262 $this->clientError(_('Not a supported data format.'));
1267 function initTwitterRss()
1270 $this->elementStart(
1274 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1275 'xmlns:georss' => 'http://www.georss.org/georss'
1278 $this->elementStart('channel');
1279 Event::handle('StartApiRss', array($this));
1282 function endTwitterRss()
1284 $this->elementEnd('channel');
1285 $this->elementEnd('rss');
1289 function initTwitterAtom()
1292 // FIXME: don't hardcode the language here!
1293 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1294 'xml:lang' => 'en-US',
1295 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1298 function endTwitterAtom()
1300 $this->elementEnd('feed');
1304 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1306 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1307 switch ($content_type) {
1309 $this->showTwitterXmlUser($profile_array);
1312 $this->showJsonObjects($profile_array);
1315 // TRANS: Client error on an API request with an unsupported data format.
1316 $this->clientError(_('Not a supported data format.'));
1321 private static function is_decimal($str)
1323 return preg_match('/^[0-9]+$/', $str);
1326 function getTargetUser($id)
1329 // Twitter supports these other ways of passing the user ID
1330 if (self::is_decimal($this->arg('id'))) {
1331 return User::getKV($this->arg('id'));
1332 } else if ($this->arg('id')) {
1333 $nickname = common_canonical_nickname($this->arg('id'));
1334 return User::getKV('nickname', $nickname);
1335 } else if ($this->arg('user_id')) {
1336 // This is to ensure that a non-numeric user_id still
1337 // overrides screen_name even if it doesn't get used
1338 if (self::is_decimal($this->arg('user_id'))) {
1339 return User::getKV('id', $this->arg('user_id'));
1341 } else if ($this->arg('screen_name')) {
1342 $nickname = common_canonical_nickname($this->arg('screen_name'));
1343 return User::getKV('nickname', $nickname);
1345 // Fall back to trying the currently authenticated user
1346 return $this->scoped->getUser();
1349 } else if (self::is_decimal($id)) {
1350 return User::getKV($id);
1352 $nickname = common_canonical_nickname($id);
1353 return User::getKV('nickname', $nickname);
1357 function getTargetProfile($id)
1361 // Twitter supports these other ways of passing the user ID
1362 if (self::is_decimal($this->arg('id'))) {
1363 return Profile::getKV($this->arg('id'));
1364 } else if ($this->arg('id')) {
1365 // Screen names currently can only uniquely identify a local user.
1366 $nickname = common_canonical_nickname($this->arg('id'));
1367 $user = User::getKV('nickname', $nickname);
1368 return $user ? $user->getProfile() : null;
1369 } else if ($this->arg('user_id')) {
1370 // This is to ensure that a non-numeric user_id still
1371 // overrides screen_name even if it doesn't get used
1372 if (self::is_decimal($this->arg('user_id'))) {
1373 return Profile::getKV('id', $this->arg('user_id'));
1375 } else if ($this->arg('screen_name')) {
1376 $nickname = common_canonical_nickname($this->arg('screen_name'));
1377 $user = User::getKV('nickname', $nickname);
1378 return $user instanceof User ? $user->getProfile() : null;
1380 // Fall back to trying the currently authenticated user
1381 return $this->scoped;
1383 } else if (self::is_decimal($id)) {
1384 return Profile::getKV($id);
1386 $nickname = common_canonical_nickname($id);
1387 $user = User::getKV('nickname', $nickname);
1388 return $user ? $user->getProfile() : null;
1392 function getTargetGroup($id)
1395 if (self::is_decimal($this->arg('id'))) {
1396 return User_group::getKV('id', $this->arg('id'));
1397 } else if ($this->arg('id')) {
1398 return User_group::getForNickname($this->arg('id'));
1399 } else if ($this->arg('group_id')) {
1400 // This is to ensure that a non-numeric group_id still
1401 // overrides group_name even if it doesn't get used
1402 if (self::is_decimal($this->arg('group_id'))) {
1403 return User_group::getKV('id', $this->arg('group_id'));
1405 } else if ($this->arg('group_name')) {
1406 return User_group::getForNickname($this->arg('group_name'));
1409 } else if (self::is_decimal($id)) {
1410 return User_group::getKV('id', $id);
1411 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1412 return User_group::getKV('uri', urldecode($this->arg('uri')));
1414 return User_group::getForNickname($id);
1418 function getTargetList($user=null, $id=null)
1420 $tagger = $this->getTargetUser($user);
1424 $id = $this->arg('id');
1428 if (is_numeric($id)) {
1429 $list = Profile_list::getKV('id', $id);
1431 // only if the list with the id belongs to the tagger
1432 if(empty($list) || $list->tagger != $tagger->id) {
1437 $tag = common_canonical_tag($id);
1438 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1441 if (!empty($list) && $list->private) {
1442 if ($this->scoped->id == $list->tagger) {
1453 * Returns query argument or default value if not found. Certain
1454 * parameters used throughout the API are lightly scrubbed and
1455 * bounds checked. This overrides Action::arg().
1457 * @param string $key requested argument
1458 * @param string $def default value to return if $key is not provided
1462 function arg($key, $def=null)
1464 // XXX: Do even more input validation/scrubbing?
1466 if (array_key_exists($key, $this->args)) {
1469 $page = (int)$this->args['page'];
1470 return ($page < 1) ? 1 : $page;
1472 $count = (int)$this->args['count'];
1475 } elseif ($count > 200) {
1481 $since_id = (int)$this->args['since_id'];
1482 return ($since_id < 1) ? 0 : $since_id;
1484 $max_id = (int)$this->args['max_id'];
1485 return ($max_id < 1) ? 0 : $max_id;
1487 return parent::arg($key, $def);
1495 * Calculate the complete URI that called up this action. Used for
1496 * Atom rel="self" links. Warning: this is funky.
1498 * @return string URL a URL suitable for rel="self" Atom links
1500 function getSelfUri()
1502 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1504 $id = $this->arg('id');
1505 $aargs = array('format' => $this->format);
1510 $user = $this->arg('user');
1511 if (!empty($user)) {
1512 $aargs['user'] = $user;
1515 $tag = $this->arg('tag');
1517 $aargs['tag'] = $tag;
1520 parse_str($_SERVER['QUERY_STRING'], $params);
1522 if (!empty($params)) {
1523 unset($params['p']);
1524 $pstring = http_build_query($params);
1527 $uri = common_local_url($action, $aargs);
1529 if (!empty($pstring)) {
1530 $uri .= '?' . $pstring;