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;
127 var $since_id = null;
129 var $callback = null;
132 var $access = self::READ_ONLY; // read (default) or read-write
134 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
139 * @param array $args Web and URL arguments
141 * @return boolean false if user doesn't exist
143 protected function prepare(array $args=array())
145 StatusNet::setApi(true); // reduce exception reports to aid in debugging
146 parent::prepare($args);
148 $this->format = $this->arg('format');
149 $this->callback = $this->arg('callback');
150 $this->page = (int)$this->arg('page', 1);
151 $this->count = (int)$this->arg('count', 20);
152 $this->max_id = (int)$this->arg('max_id', 0);
153 $this->since_id = (int)$this->arg('since_id', 0);
155 if ($this->arg('since')) {
156 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
159 $this->source = $this->trimmed('source');
161 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
162 $this->source = 'api';
171 * @param array $args Arguments from $_REQUEST
175 protected function handle()
177 header('Access-Control-Allow-Origin: *');
182 * Overrides XMLOutputter::element to write booleans as strings (true|false).
183 * See that method's documentation for more info.
185 * @param string $tag Element type or tagname
186 * @param array $attrs Array of element attributes, as
188 * @param string $content string content of the element
192 function element($tag, $attrs=null, $content=null)
194 if (is_bool($content)) {
195 $content = ($content ? 'true' : 'false');
198 return parent::element($tag, $attrs, $content);
201 function twitterUserArray($profile, $get_notice=false)
203 $twitter_user = array();
206 $user = $profile->getUser();
207 } catch (NoSuchUserException $e) {
211 $twitter_user['id'] = intval($profile->id);
212 $twitter_user['name'] = $profile->getBestName();
213 $twitter_user['screen_name'] = $profile->nickname;
214 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
215 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
217 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
218 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
219 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
221 // START introduced by qvitter API, not necessary for StatusNet API
222 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
224 $avatar = Avatar::getUploaded($profile);
225 $origurl = $avatar->displayUrl();
226 } catch (Exception $e) {
227 $origurl = $twitter_user['profile_image_url_profile_size'];
229 $twitter_user['profile_image_url_original'] = $origurl;
231 $twitter_user['groups_count'] = $profile->getGroupCount();
232 foreach (array('linkcolor', 'backgroundcolor') as $key) {
233 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
235 // END introduced by qvitter API, not necessary for StatusNet API
237 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
238 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
239 $twitter_user['followers_count'] = $profile->subscriberCount();
241 // Note: some profiles don't have an associated user
243 $twitter_user['friends_count'] = $profile->subscriptionCount();
245 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
249 if (!empty($user) && $user->timezone) {
250 $timezone = $user->timezone;
254 $t->setTimezone(new DateTimeZone($timezone));
256 $twitter_user['utc_offset'] = $t->format('Z');
257 $twitter_user['time_zone'] = $timezone;
258 $twitter_user['statuses_count'] = $profile->noticeCount();
260 // Is the requesting user following this user?
261 $twitter_user['following'] = false;
262 $twitter_user['statusnet_blocking'] = false;
263 $twitter_user['notifications'] = false;
265 if (isset($this->auth_user)) {
267 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
268 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
271 $sub = Subscription::pkeyGet(array('subscriber' =>
272 $this->auth_user->id,
273 'subscribed' => $profile->id));
276 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
281 $notice = $profile->getCurrentNotice();
282 if ($notice instanceof Notice) {
284 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
288 // StatusNet-specific
290 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
292 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
293 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
295 return $twitter_user;
298 function twitterStatusArray($notice, $include_user=true)
300 $base = $this->twitterSimpleStatusArray($notice, $include_user);
302 if (!empty($notice->repeat_of)) {
303 $original = Notice::getKV('id', $notice->repeat_of);
304 if ($original instanceof Notice) {
305 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
306 $base['retweeted_status'] = $orig_array;
313 function twitterSimpleStatusArray($notice, $include_user=true)
315 $profile = $notice->getProfile();
317 $twitter_status = array();
318 $twitter_status['text'] = $notice->content;
319 $twitter_status['truncated'] = false; # Not possible on StatusNet
320 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
322 // We could just do $notice->reply_to but maybe the future holds a
323 // different story for parenting.
324 $parent = $notice->getParent();
325 $in_reply_to = $parent->id;
326 } catch (Exception $e) {
329 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
333 $ns = $notice->getSource();
334 if ($ns instanceof Notice_source) {
335 if (!empty($ns->name) && !empty($ns->url)) {
336 $source = '<a href="'
337 . htmlspecialchars($ns->url)
338 . '" rel="nofollow">'
339 . htmlspecialchars($ns->name)
346 $twitter_status['uri'] = $notice->getUri();
347 $twitter_status['source'] = $source;
348 $twitter_status['id'] = intval($notice->id);
350 $replier_profile = null;
352 if ($notice->reply_to) {
353 $reply = Notice::getKV(intval($notice->reply_to));
355 $replier_profile = $reply->getProfile();
359 $twitter_status['in_reply_to_user_id'] =
360 ($replier_profile) ? intval($replier_profile->id) : null;
361 $twitter_status['in_reply_to_screen_name'] =
362 ($replier_profile) ? $replier_profile->nickname : null;
364 if (isset($notice->lat) && isset($notice->lon)) {
365 // This is the format that GeoJSON expects stuff to be in
366 $twitter_status['geo'] = array('type' => 'Point',
367 'coordinates' => array((float) $notice->lat,
368 (float) $notice->lon));
370 $twitter_status['geo'] = null;
373 if (!is_null($this->scoped)) {
374 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
376 $twitter_status['repeated'] = false;
380 $attachments = $notice->attachments();
382 if (!empty($attachments)) {
384 $twitter_status['attachments'] = array();
386 foreach ($attachments as $attachment) {
388 $enclosure_o = $attachment->getEnclosure();
389 $enclosure = array();
390 $enclosure['url'] = $enclosure_o->url;
391 $enclosure['mimetype'] = $enclosure_o->mimetype;
392 $enclosure['size'] = $enclosure_o->size;
393 $twitter_status['attachments'][] = $enclosure;
394 } catch (ServerException $e) {
395 // There was not enough metadata available
400 if ($include_user && $profile) {
401 // Don't get notice (recursive!)
402 $twitter_user = $this->twitterUserArray($profile, false);
403 $twitter_status['user'] = $twitter_user;
406 // StatusNet-specific
408 $twitter_status['statusnet_html'] = $notice->rendered;
409 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
411 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
412 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
413 array('include_user'=>$include_user)));
415 return $twitter_status;
418 function twitterGroupArray($group)
420 $twitter_group = array();
422 $twitter_group['id'] = intval($group->id);
423 $twitter_group['url'] = $group->permalink();
424 $twitter_group['nickname'] = $group->nickname;
425 $twitter_group['fullname'] = $group->fullname;
427 if (isset($this->auth_user)) {
428 $twitter_group['member'] = $this->auth_user->isMember($group);
429 $twitter_group['blocked'] = Group_block::isBlocked(
431 $this->auth_user->getProfile()
435 $twitter_group['admin_count'] = $group->getAdminCount();
436 $twitter_group['member_count'] = $group->getMemberCount();
437 $twitter_group['original_logo'] = $group->original_logo;
438 $twitter_group['homepage_logo'] = $group->homepage_logo;
439 $twitter_group['stream_logo'] = $group->stream_logo;
440 $twitter_group['mini_logo'] = $group->mini_logo;
441 $twitter_group['homepage'] = $group->homepage;
442 $twitter_group['description'] = $group->description;
443 $twitter_group['location'] = $group->location;
444 $twitter_group['created'] = $this->dateTwitter($group->created);
445 $twitter_group['modified'] = $this->dateTwitter($group->modified);
447 return $twitter_group;
450 function twitterRssGroupArray($group)
453 $entry['content']=$group->description;
454 $entry['title']=$group->nickname;
455 $entry['link']=$group->permalink();
456 $entry['published']=common_date_iso8601($group->created);
457 $entry['updated']==common_date_iso8601($group->modified);
458 $taguribase = common_config('integration', 'groupuri');
459 $entry['id'] = "group:$groupuribase:$entry[link]";
461 $entry['description'] = $entry['content'];
462 $entry['pubDate'] = common_date_rfc2822($group->created);
463 $entry['guid'] = $entry['link'];
468 function twitterListArray($list)
470 $profile = Profile::getKV('id', $list->tagger);
472 $twitter_list = array();
473 $twitter_list['id'] = $list->id;
474 $twitter_list['name'] = $list->tag;
475 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
476 $twitter_list['slug'] = $list->tag;
477 $twitter_list['description'] = $list->description;
478 $twitter_list['subscriber_count'] = $list->subscriberCount();
479 $twitter_list['member_count'] = $list->taggedCount();
480 $twitter_list['uri'] = $list->getUri();
482 if (isset($this->auth_user)) {
483 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
485 $twitter_list['following'] = false;
488 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
489 $twitter_list['user'] = $this->twitterUserArray($profile, false);
491 return $twitter_list;
494 function twitterRssEntryArray($notice)
498 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
499 $profile = $notice->getProfile();
501 // We trim() to avoid extraneous whitespace in the output
503 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
504 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
505 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
506 $entry['published'] = common_date_iso8601($notice->created);
508 $taguribase = TagURI::base();
509 $entry['id'] = "tag:$taguribase:$entry[link]";
511 $entry['updated'] = $entry['published'];
512 $entry['author'] = $profile->getBestName();
515 $attachments = $notice->attachments();
516 $enclosures = array();
518 foreach ($attachments as $attachment) {
520 $enclosure_o = $attachment->getEnclosure();
521 $enclosure = array();
522 $enclosure['url'] = $enclosure_o->url;
523 $enclosure['mimetype'] = $enclosure_o->mimetype;
524 $enclosure['size'] = $enclosure_o->size;
525 $enclosures[] = $enclosure;
526 } catch (ServerException $e) {
527 // There was not enough metadata available
531 if (!empty($enclosures)) {
532 $entry['enclosures'] = $enclosures;
536 $tag = new Notice_tag();
537 $tag->notice_id = $notice->id;
539 $entry['tags']=array();
540 while ($tag->fetch()) {
541 $entry['tags'][]=$tag->tag;
547 $entry['description'] = $entry['content'];
548 $entry['pubDate'] = common_date_rfc2822($notice->created);
549 $entry['guid'] = $entry['link'];
551 if (isset($notice->lat) && isset($notice->lon)) {
552 // This is the format that GeoJSON expects stuff to be in.
553 // showGeoRSS() below uses it for XML output, so we reuse it
554 $entry['geo'] = array('type' => 'Point',
555 'coordinates' => array((float) $notice->lat,
556 (float) $notice->lon));
558 $entry['geo'] = null;
561 Event::handle('EndRssEntryArray', array($notice, &$entry));
567 function twitterRelationshipArray($source, $target)
569 $relationship = array();
571 $relationship['source'] =
572 $this->relationshipDetailsArray($source, $target);
573 $relationship['target'] =
574 $this->relationshipDetailsArray($target, $source);
576 return array('relationship' => $relationship);
579 function relationshipDetailsArray($source, $target)
583 $source_profile = $source->getProfile();
584 $target_profile = $target->getProfile();
586 $details['screen_name'] = $source->nickname;
587 $details['followed_by'] = $target->isSubscribed($source_profile);
588 $details['following'] = $source->isSubscribed($target_profile);
590 $notifications = false;
592 if ($source->isSubscribed($target_profile)) {
593 $sub = Subscription::pkeyGet(array('subscriber' =>
594 $source->id, 'subscribed' => $target->id));
597 $notifications = ($sub->jabber || $sub->sms);
601 $details['notifications_enabled'] = $notifications;
602 $details['blocking'] = $source->hasBlocked($target_profile);
603 $details['id'] = intval($source->id);
608 function showTwitterXmlRelationship($relationship)
610 $this->elementStart('relationship');
612 foreach($relationship as $element => $value) {
613 if ($element == 'source' || $element == 'target') {
614 $this->elementStart($element);
615 $this->showXmlRelationshipDetails($value);
616 $this->elementEnd($element);
620 $this->elementEnd('relationship');
623 function showXmlRelationshipDetails($details)
625 foreach($details as $element => $value) {
626 $this->element($element, null, $value);
630 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
634 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
636 $this->elementStart($tag, $attrs);
637 foreach($twitter_status as $element => $value) {
640 $this->showTwitterXmlUser($twitter_status['user']);
643 $this->element($element, null, common_xml_safe_str($value));
646 $this->showXmlAttachments($twitter_status['attachments']);
649 $this->showGeoXML($value);
651 case 'retweeted_status':
652 $this->showTwitterXmlStatus($value, 'retweeted_status');
655 if (strncmp($element, 'statusnet_', 10) == 0) {
656 $this->element('statusnet:'.substr($element, 10), null, $value);
658 $this->element($element, null, $value);
662 $this->elementEnd($tag);
665 function showTwitterXmlGroup($twitter_group)
667 $this->elementStart('group');
668 foreach($twitter_group as $element => $value) {
669 $this->element($element, null, $value);
671 $this->elementEnd('group');
674 function showTwitterXmlList($twitter_list)
676 $this->elementStart('list');
677 foreach($twitter_list as $element => $value) {
678 if($element == 'user') {
679 $this->showTwitterXmlUser($value, 'user');
682 $this->element($element, null, $value);
685 $this->elementEnd('list');
688 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
692 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
694 $this->elementStart($role, $attrs);
695 foreach($twitter_user as $element => $value) {
696 if ($element == 'status') {
697 $this->showTwitterXmlStatus($twitter_user['status']);
698 } else if (strncmp($element, 'statusnet_', 10) == 0) {
699 $this->element('statusnet:'.substr($element, 10), null, $value);
701 $this->element($element, null, $value);
704 $this->elementEnd($role);
707 function showXmlAttachments($attachments) {
708 if (!empty($attachments)) {
709 $this->elementStart('attachments', array('type' => 'array'));
710 foreach ($attachments as $attachment) {
712 $attrs['url'] = $attachment['url'];
713 $attrs['mimetype'] = $attachment['mimetype'];
714 $attrs['size'] = $attachment['size'];
715 $this->element('enclosure', $attrs, '');
717 $this->elementEnd('attachments');
721 function showGeoXML($geo)
725 $this->element('geo');
727 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
728 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
729 $this->elementEnd('geo');
733 function showGeoRSS($geo)
739 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
744 function showTwitterRssItem($entry)
746 $this->elementStart('item');
747 $this->element('title', null, $entry['title']);
748 $this->element('description', null, $entry['description']);
749 $this->element('pubDate', null, $entry['pubDate']);
750 $this->element('guid', null, $entry['guid']);
751 $this->element('link', null, $entry['link']);
753 // RSS only supports 1 enclosure per item
754 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
755 $enclosure = $entry['enclosures'][0];
756 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
759 if(array_key_exists('tags', $entry)){
760 foreach($entry['tags'] as $tag){
761 $this->element('category', null,$tag);
765 $this->showGeoRSS($entry['geo']);
766 $this->elementEnd('item');
769 function showJsonObjects($objects)
771 print(json_encode($objects));
774 function showSingleXmlStatus($notice)
776 $this->initDocument('xml');
777 $twitter_status = $this->twitterStatusArray($notice);
778 $this->showTwitterXmlStatus($twitter_status, 'status', true);
779 $this->endDocument('xml');
782 function showSingleAtomStatus($notice)
784 header('Content-Type: application/atom+xml; charset=utf-8');
785 print $notice->asAtomEntry(true, true, true, $this->auth_user);
788 function show_single_json_status($notice)
790 $this->initDocument('json');
791 $status = $this->twitterStatusArray($notice);
792 $this->showJsonObjects($status);
793 $this->endDocument('json');
796 function showXmlTimeline($notice)
798 $this->initDocument('xml');
799 $this->elementStart('statuses', array('type' => 'array',
800 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
802 if (is_array($notice)) {
803 $notice = new ArrayWrapper($notice);
806 while ($notice->fetch()) {
808 $twitter_status = $this->twitterStatusArray($notice);
809 $this->showTwitterXmlStatus($twitter_status);
810 } catch (Exception $e) {
811 common_log(LOG_ERR, $e->getMessage());
816 $this->elementEnd('statuses');
817 $this->endDocument('xml');
820 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
822 $this->initDocument('rss');
824 $this->element('title', null, $title);
825 $this->element('link', null, $link);
827 if (!is_null($self)) {
831 'type' => 'application/rss+xml',
838 if (!is_null($suplink)) {
839 // For FriendFeed's SUP protocol
840 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
841 'rel' => 'http://api.friendfeed.com/2008/03#sup',
843 'type' => 'application/json'));
846 if (!is_null($logo)) {
847 $this->elementStart('image');
848 $this->element('link', null, $link);
849 $this->element('title', null, $title);
850 $this->element('url', null, $logo);
851 $this->elementEnd('image');
854 $this->element('description', null, $subtitle);
855 $this->element('language', null, 'en-us');
856 $this->element('ttl', null, '40');
858 if (is_array($notice)) {
859 $notice = new ArrayWrapper($notice);
862 while ($notice->fetch()) {
864 $entry = $this->twitterRssEntryArray($notice);
865 $this->showTwitterRssItem($entry);
866 } catch (Exception $e) {
867 common_log(LOG_ERR, $e->getMessage());
868 // continue on exceptions
872 $this->endTwitterRss();
875 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
877 $this->initDocument('atom');
879 $this->element('title', null, $title);
880 $this->element('id', null, $id);
881 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
883 if (!is_null($logo)) {
884 $this->element('logo',null,$logo);
887 if (!is_null($suplink)) {
888 // For FriendFeed's SUP protocol
889 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
891 'type' => 'application/json'));
894 if (!is_null($selfuri)) {
895 $this->element('link', array('href' => $selfuri,
896 'rel' => 'self', 'type' => 'application/atom+xml'), null);
899 $this->element('updated', null, common_date_iso8601('now'));
900 $this->element('subtitle', null, $subtitle);
902 if (is_array($notice)) {
903 $notice = new ArrayWrapper($notice);
906 while ($notice->fetch()) {
908 $this->raw($notice->asAtomEntry());
909 } catch (Exception $e) {
910 common_log(LOG_ERR, $e->getMessage());
915 $this->endDocument('atom');
918 function showRssGroups($group, $title, $link, $subtitle)
920 $this->initDocument('rss');
922 $this->element('title', null, $title);
923 $this->element('link', null, $link);
924 $this->element('description', null, $subtitle);
925 $this->element('language', null, 'en-us');
926 $this->element('ttl', null, '40');
928 if (is_array($group)) {
929 foreach ($group as $g) {
930 $twitter_group = $this->twitterRssGroupArray($g);
931 $this->showTwitterRssItem($twitter_group);
934 while ($group->fetch()) {
935 $twitter_group = $this->twitterRssGroupArray($group);
936 $this->showTwitterRssItem($twitter_group);
940 $this->endTwitterRss();
943 function showTwitterAtomEntry($entry)
945 $this->elementStart('entry');
946 $this->element('title', null, common_xml_safe_str($entry['title']));
949 array('type' => 'html'),
950 common_xml_safe_str($entry['content'])
952 $this->element('id', null, $entry['id']);
953 $this->element('published', null, $entry['published']);
954 $this->element('updated', null, $entry['updated']);
955 $this->element('link', array('type' => 'text/html',
956 'href' => $entry['link'],
957 'rel' => 'alternate'));
958 $this->element('link', array('type' => $entry['avatar-type'],
959 'href' => $entry['avatar'],
961 $this->elementStart('author');
963 $this->element('name', null, $entry['author-name']);
964 $this->element('uri', null, $entry['author-uri']);
966 $this->elementEnd('author');
967 $this->elementEnd('entry');
970 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
972 $this->initDocument('atom');
974 $this->element('title', null, common_xml_safe_str($title));
975 $this->element('id', null, $id);
976 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
978 if (!is_null($selfuri)) {
979 $this->element('link', array('href' => $selfuri,
980 'rel' => 'self', 'type' => 'application/atom+xml'), null);
983 $this->element('updated', null, common_date_iso8601('now'));
984 $this->element('subtitle', null, common_xml_safe_str($subtitle));
986 if (is_array($group)) {
987 foreach ($group as $g) {
988 $this->raw($g->asAtomEntry());
991 while ($group->fetch()) {
992 $this->raw($group->asAtomEntry());
996 $this->endDocument('atom');
1000 function showJsonTimeline($notice)
1002 $this->initDocument('json');
1004 $statuses = array();
1006 if (is_array($notice)) {
1007 $notice = new ArrayWrapper($notice);
1010 while ($notice->fetch()) {
1012 $twitter_status = $this->twitterStatusArray($notice);
1013 array_push($statuses, $twitter_status);
1014 } catch (Exception $e) {
1015 common_log(LOG_ERR, $e->getMessage());
1020 $this->showJsonObjects($statuses);
1022 $this->endDocument('json');
1025 function showJsonGroups($group)
1027 $this->initDocument('json');
1031 if (is_array($group)) {
1032 foreach ($group as $g) {
1033 $twitter_group = $this->twitterGroupArray($g);
1034 array_push($groups, $twitter_group);
1037 while ($group->fetch()) {
1038 $twitter_group = $this->twitterGroupArray($group);
1039 array_push($groups, $twitter_group);
1043 $this->showJsonObjects($groups);
1045 $this->endDocument('json');
1048 function showXmlGroups($group)
1051 $this->initDocument('xml');
1052 $this->elementStart('groups', array('type' => 'array'));
1054 if (is_array($group)) {
1055 foreach ($group as $g) {
1056 $twitter_group = $this->twitterGroupArray($g);
1057 $this->showTwitterXmlGroup($twitter_group);
1060 while ($group->fetch()) {
1061 $twitter_group = $this->twitterGroupArray($group);
1062 $this->showTwitterXmlGroup($twitter_group);
1066 $this->elementEnd('groups');
1067 $this->endDocument('xml');
1070 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1073 $this->initDocument('xml');
1074 $this->elementStart('lists_list');
1075 $this->elementStart('lists', array('type' => 'array'));
1077 if (is_array($list)) {
1078 foreach ($list as $l) {
1079 $twitter_list = $this->twitterListArray($l);
1080 $this->showTwitterXmlList($twitter_list);
1083 while ($list->fetch()) {
1084 $twitter_list = $this->twitterListArray($list);
1085 $this->showTwitterXmlList($twitter_list);
1089 $this->elementEnd('lists');
1091 $this->element('next_cursor', null, $next_cursor);
1092 $this->element('previous_cursor', null, $prev_cursor);
1094 $this->elementEnd('lists_list');
1095 $this->endDocument('xml');
1098 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1100 $this->initDocument('json');
1104 if (is_array($list)) {
1105 foreach ($list as $l) {
1106 $twitter_list = $this->twitterListArray($l);
1107 array_push($lists, $twitter_list);
1110 while ($list->fetch()) {
1111 $twitter_list = $this->twitterListArray($list);
1112 array_push($lists, $twitter_list);
1116 $lists_list = array(
1118 'next_cursor' => $next_cursor,
1119 'next_cursor_str' => strval($next_cursor),
1120 'previous_cursor' => $prev_cursor,
1121 'previous_cursor_str' => strval($prev_cursor)
1124 $this->showJsonObjects($lists_list);
1126 $this->endDocument('json');
1129 function showTwitterXmlUsers($user)
1131 $this->initDocument('xml');
1132 $this->elementStart('users', array('type' => 'array',
1133 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1135 if (is_array($user)) {
1136 foreach ($user as $u) {
1137 $twitter_user = $this->twitterUserArray($u);
1138 $this->showTwitterXmlUser($twitter_user);
1141 while ($user->fetch()) {
1142 $twitter_user = $this->twitterUserArray($user);
1143 $this->showTwitterXmlUser($twitter_user);
1147 $this->elementEnd('users');
1148 $this->endDocument('xml');
1151 function showJsonUsers($user)
1153 $this->initDocument('json');
1157 if (is_array($user)) {
1158 foreach ($user as $u) {
1159 $twitter_user = $this->twitterUserArray($u);
1160 array_push($users, $twitter_user);
1163 while ($user->fetch()) {
1164 $twitter_user = $this->twitterUserArray($user);
1165 array_push($users, $twitter_user);
1169 $this->showJsonObjects($users);
1171 $this->endDocument('json');
1174 function showSingleJsonGroup($group)
1176 $this->initDocument('json');
1177 $twitter_group = $this->twitterGroupArray($group);
1178 $this->showJsonObjects($twitter_group);
1179 $this->endDocument('json');
1182 function showSingleXmlGroup($group)
1184 $this->initDocument('xml');
1185 $twitter_group = $this->twitterGroupArray($group);
1186 $this->showTwitterXmlGroup($twitter_group);
1187 $this->endDocument('xml');
1190 function showSingleJsonList($list)
1192 $this->initDocument('json');
1193 $twitter_list = $this->twitterListArray($list);
1194 $this->showJsonObjects($twitter_list);
1195 $this->endDocument('json');
1198 function showSingleXmlList($list)
1200 $this->initDocument('xml');
1201 $twitter_list = $this->twitterListArray($list);
1202 $this->showTwitterXmlList($twitter_list);
1203 $this->endDocument('xml');
1206 function dateTwitter($dt)
1208 $dateStr = date('d F Y H:i:s', strtotime($dt));
1209 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1210 $d->setTimezone(new DateTimeZone(common_timezone()));
1211 return $d->format('D M d H:i:s O Y');
1214 function initDocument($type='xml')
1218 header('Content-Type: application/xml; charset=utf-8');
1222 header('Content-Type: application/json; charset=utf-8');
1224 // Check for JSONP callback
1225 if (isset($this->callback)) {
1226 print $this->callback . '(';
1230 header("Content-Type: application/rss+xml; charset=utf-8");
1231 $this->initTwitterRss();
1234 header('Content-Type: application/atom+xml; charset=utf-8');
1235 $this->initTwitterAtom();
1238 // TRANS: Client error on an API request with an unsupported data format.
1239 $this->clientError(_('Not a supported data format.'));
1245 function endDocument($type='xml')
1252 // Check for JSONP callback
1253 if (isset($this->callback)) {
1258 $this->endTwitterRss();
1261 $this->endTwitterRss();
1264 // TRANS: Client error on an API request with an unsupported data format.
1265 $this->clientError(_('Not a supported data format.'));
1270 function initTwitterRss()
1273 $this->elementStart(
1277 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1278 'xmlns:georss' => 'http://www.georss.org/georss'
1281 $this->elementStart('channel');
1282 Event::handle('StartApiRss', array($this));
1285 function endTwitterRss()
1287 $this->elementEnd('channel');
1288 $this->elementEnd('rss');
1292 function initTwitterAtom()
1295 // FIXME: don't hardcode the language here!
1296 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1297 'xml:lang' => 'en-US',
1298 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1301 function endTwitterAtom()
1303 $this->elementEnd('feed');
1307 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1309 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1310 switch ($content_type) {
1312 $this->showTwitterXmlUser($profile_array);
1315 $this->showJsonObjects($profile_array);
1318 // TRANS: Client error on an API request with an unsupported data format.
1319 $this->clientError(_('Not a supported data format.'));
1324 private static function is_decimal($str)
1326 return preg_match('/^[0-9]+$/', $str);
1329 function getTargetUser($id)
1332 // Twitter supports these other ways of passing the user ID
1333 if (self::is_decimal($this->arg('id'))) {
1334 return User::getKV($this->arg('id'));
1335 } else if ($this->arg('id')) {
1336 $nickname = common_canonical_nickname($this->arg('id'));
1337 return User::getKV('nickname', $nickname);
1338 } else if ($this->arg('user_id')) {
1339 // This is to ensure that a non-numeric user_id still
1340 // overrides screen_name even if it doesn't get used
1341 if (self::is_decimal($this->arg('user_id'))) {
1342 return User::getKV('id', $this->arg('user_id'));
1344 } else if ($this->arg('screen_name')) {
1345 $nickname = common_canonical_nickname($this->arg('screen_name'));
1346 return User::getKV('nickname', $nickname);
1348 // Fall back to trying the currently authenticated user
1349 return $this->auth_user;
1352 } else if (self::is_decimal($id)) {
1353 return User::getKV($id);
1355 $nickname = common_canonical_nickname($id);
1356 return User::getKV('nickname', $nickname);
1360 function getTargetProfile($id)
1364 // Twitter supports these other ways of passing the user ID
1365 if (self::is_decimal($this->arg('id'))) {
1366 return Profile::getKV($this->arg('id'));
1367 } else if ($this->arg('id')) {
1368 // Screen names currently can only uniquely identify a local user.
1369 $nickname = common_canonical_nickname($this->arg('id'));
1370 $user = User::getKV('nickname', $nickname);
1371 return $user ? $user->getProfile() : null;
1372 } else if ($this->arg('user_id')) {
1373 // This is to ensure that a non-numeric user_id still
1374 // overrides screen_name even if it doesn't get used
1375 if (self::is_decimal($this->arg('user_id'))) {
1376 return Profile::getKV('id', $this->arg('user_id'));
1378 } else if ($this->arg('screen_name')) {
1379 $nickname = common_canonical_nickname($this->arg('screen_name'));
1380 $user = User::getKV('nickname', $nickname);
1381 return $user instanceof User ? $user->getProfile() : null;
1383 // Fall back to trying the currently authenticated user
1384 return $this->scoped;
1386 } else if (self::is_decimal($id)) {
1387 return Profile::getKV($id);
1389 $nickname = common_canonical_nickname($id);
1390 $user = User::getKV('nickname', $nickname);
1391 return $user ? $user->getProfile() : null;
1395 function getTargetGroup($id)
1398 if (self::is_decimal($this->arg('id'))) {
1399 return User_group::getKV('id', $this->arg('id'));
1400 } else if ($this->arg('id')) {
1401 return User_group::getForNickname($this->arg('id'));
1402 } else if ($this->arg('group_id')) {
1403 // This is to ensure that a non-numeric group_id still
1404 // overrides group_name even if it doesn't get used
1405 if (self::is_decimal($this->arg('group_id'))) {
1406 return User_group::getKV('id', $this->arg('group_id'));
1408 } else if ($this->arg('group_name')) {
1409 return User_group::getForNickname($this->arg('group_name'));
1412 } else if (self::is_decimal($id)) {
1413 return User_group::getKV('id', $id);
1414 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1415 return User_group::getKV('uri', urldecode($this->arg('uri')));
1417 return User_group::getForNickname($id);
1421 function getTargetList($user=null, $id=null)
1423 $tagger = $this->getTargetUser($user);
1427 $id = $this->arg('id');
1431 if (is_numeric($id)) {
1432 $list = Profile_list::getKV('id', $id);
1434 // only if the list with the id belongs to the tagger
1435 if(empty($list) || $list->tagger != $tagger->id) {
1440 $tag = common_canonical_tag($id);
1441 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1444 if (!empty($list) && $list->private) {
1445 if ($this->auth_user->id == $list->tagger) {
1456 * Returns query argument or default value if not found. Certain
1457 * parameters used throughout the API are lightly scrubbed and
1458 * bounds checked. This overrides Action::arg().
1460 * @param string $key requested argument
1461 * @param string $def default value to return if $key is not provided
1465 function arg($key, $def=null)
1467 // XXX: Do even more input validation/scrubbing?
1469 if (array_key_exists($key, $this->args)) {
1472 $page = (int)$this->args['page'];
1473 return ($page < 1) ? 1 : $page;
1475 $count = (int)$this->args['count'];
1478 } elseif ($count > 200) {
1484 $since_id = (int)$this->args['since_id'];
1485 return ($since_id < 1) ? 0 : $since_id;
1487 $max_id = (int)$this->args['max_id'];
1488 return ($max_id < 1) ? 0 : $max_id;
1490 return parent::arg($key, $def);
1498 * Calculate the complete URI that called up this action. Used for
1499 * Atom rel="self" links. Warning: this is funky.
1501 * @return string URL a URL suitable for rel="self" Atom links
1503 function getSelfUri()
1505 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1507 $id = $this->arg('id');
1508 $aargs = array('format' => $this->format);
1513 $tag = $this->arg('tag');
1515 $aargs['tag'] = $tag;
1518 parse_str($_SERVER['QUERY_STRING'], $params);
1520 if (!empty($params)) {
1521 unset($params['p']);
1522 $pstring = http_build_query($params);
1525 $uri = common_local_url($action, $aargs);
1527 if (!empty($pstring)) {
1528 $uri .= '?' . $pstring;