3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009-2010 StatusNet, Inc.
31 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33 * @link http://status.net/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
77 The URLs for methods referred to in this API documentation are
78 relative to the StatusNet API root. The API root is determined by the
79 site's @b server and @b path variables, which are generally specified
80 in config.php. For example:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
108 * @author Craig Andrews <candrews@integralblue.com>
109 * @author Dan Moore <dan@moore.cx>
110 * @author Evan Prodromou <evan@status.net>
111 * @author Jeffery To <jeffery.to@gmail.com>
112 * @author Toby Inkster <mail@tobyinkster.co.uk>
113 * @author Zach Copley <zach@status.net>
114 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115 * @link http://status.net/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
129 var $callback = null;
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();
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 $details['screen_name'] = $source->nickname;
584 $details['followed_by'] = $target->isSubscribed($source);
585 $details['following'] = $source->isSubscribed($target);
587 $notifications = false;
589 if ($source->isSubscribed($target)) {
590 $sub = Subscription::pkeyGet(array('subscriber' =>
591 $source->id, 'subscribed' => $target->id));
594 $notifications = ($sub->jabber || $sub->sms);
598 $details['notifications_enabled'] = $notifications;
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->auth_user);
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 showXmlDirectMessage($dm, $namespaces=false)
971 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
973 $this->elementStart('direct_message', $attrs);
974 foreach($dm as $element => $value) {
978 $this->showTwitterXmlUser($value, $element);
981 $this->element($element, null, common_xml_safe_str($value));
984 $this->element($element, null, $value);
988 $this->elementEnd('direct_message');
991 function directMessageArray($message)
995 $from_profile = $message->getFrom();
996 $to_profile = $message->getTo();
998 $dmsg['id'] = intval($message->id);
999 $dmsg['sender_id'] = intval($from_profile->id);
1000 $dmsg['text'] = trim($message->content);
1001 $dmsg['recipient_id'] = intval($to_profile->id);
1002 $dmsg['created_at'] = $this->dateTwitter($message->created);
1003 $dmsg['sender_screen_name'] = $from_profile->nickname;
1004 $dmsg['recipient_screen_name'] = $to_profile->nickname;
1005 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
1006 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1011 function rssDirectMessageArray($message)
1015 $from = $message->getFrom();
1017 $entry['title'] = sprintf('Message from %1$s to %2$s',
1018 $from->nickname, $message->getTo()->nickname);
1020 $entry['content'] = common_xml_safe_str($message->rendered);
1021 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1022 $entry['published'] = common_date_iso8601($message->created);
1024 $taguribase = TagURI::base();
1026 $entry['id'] = "tag:$taguribase:$entry[link]";
1027 $entry['updated'] = $entry['published'];
1029 $entry['author-name'] = $from->getBestName();
1030 $entry['author-uri'] = $from->homepage;
1032 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1034 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1035 $entry['avatar-type'] = $avatar->mediatype;
1036 } catch (Exception $e) {
1037 $entry['avatar-type'] = 'image/png';
1040 // RSS item specific
1042 $entry['description'] = $entry['content'];
1043 $entry['pubDate'] = common_date_rfc2822($message->created);
1044 $entry['guid'] = $entry['link'];
1049 function showSingleXmlDirectMessage($message)
1051 $this->initDocument('xml');
1052 $dmsg = $this->directMessageArray($message);
1053 $this->showXmlDirectMessage($dmsg, true);
1054 $this->endDocument('xml');
1057 function showSingleJsonDirectMessage($message)
1059 $this->initDocument('json');
1060 $dmsg = $this->directMessageArray($message);
1061 $this->showJsonObjects($dmsg);
1062 $this->endDocument('json');
1065 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1067 $this->initDocument('atom');
1069 $this->element('title', null, common_xml_safe_str($title));
1070 $this->element('id', null, $id);
1071 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1073 if (!is_null($selfuri)) {
1074 $this->element('link', array('href' => $selfuri,
1075 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1078 $this->element('updated', null, common_date_iso8601('now'));
1079 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1081 if (is_array($group)) {
1082 foreach ($group as $g) {
1083 $this->raw($g->asAtomEntry());
1086 while ($group->fetch()) {
1087 $this->raw($group->asAtomEntry());
1091 $this->endDocument('atom');
1095 function showJsonTimeline($notice)
1097 $this->initDocument('json');
1099 $statuses = array();
1101 if (is_array($notice)) {
1102 $notice = new ArrayWrapper($notice);
1105 while ($notice->fetch()) {
1107 $twitter_status = $this->twitterStatusArray($notice);
1108 array_push($statuses, $twitter_status);
1109 } catch (Exception $e) {
1110 common_log(LOG_ERR, $e->getMessage());
1115 $this->showJsonObjects($statuses);
1117 $this->endDocument('json');
1120 function showJsonGroups($group)
1122 $this->initDocument('json');
1126 if (is_array($group)) {
1127 foreach ($group as $g) {
1128 $twitter_group = $this->twitterGroupArray($g);
1129 array_push($groups, $twitter_group);
1132 while ($group->fetch()) {
1133 $twitter_group = $this->twitterGroupArray($group);
1134 array_push($groups, $twitter_group);
1138 $this->showJsonObjects($groups);
1140 $this->endDocument('json');
1143 function showXmlGroups($group)
1146 $this->initDocument('xml');
1147 $this->elementStart('groups', array('type' => 'array'));
1149 if (is_array($group)) {
1150 foreach ($group as $g) {
1151 $twitter_group = $this->twitterGroupArray($g);
1152 $this->showTwitterXmlGroup($twitter_group);
1155 while ($group->fetch()) {
1156 $twitter_group = $this->twitterGroupArray($group);
1157 $this->showTwitterXmlGroup($twitter_group);
1161 $this->elementEnd('groups');
1162 $this->endDocument('xml');
1165 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1168 $this->initDocument('xml');
1169 $this->elementStart('lists_list');
1170 $this->elementStart('lists', array('type' => 'array'));
1172 if (is_array($list)) {
1173 foreach ($list as $l) {
1174 $twitter_list = $this->twitterListArray($l);
1175 $this->showTwitterXmlList($twitter_list);
1178 while ($list->fetch()) {
1179 $twitter_list = $this->twitterListArray($list);
1180 $this->showTwitterXmlList($twitter_list);
1184 $this->elementEnd('lists');
1186 $this->element('next_cursor', null, $next_cursor);
1187 $this->element('previous_cursor', null, $prev_cursor);
1189 $this->elementEnd('lists_list');
1190 $this->endDocument('xml');
1193 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1195 $this->initDocument('json');
1199 if (is_array($list)) {
1200 foreach ($list as $l) {
1201 $twitter_list = $this->twitterListArray($l);
1202 array_push($lists, $twitter_list);
1205 while ($list->fetch()) {
1206 $twitter_list = $this->twitterListArray($list);
1207 array_push($lists, $twitter_list);
1211 $lists_list = array(
1213 'next_cursor' => $next_cursor,
1214 'next_cursor_str' => strval($next_cursor),
1215 'previous_cursor' => $prev_cursor,
1216 'previous_cursor_str' => strval($prev_cursor)
1219 $this->showJsonObjects($lists_list);
1221 $this->endDocument('json');
1224 function showTwitterXmlUsers($user)
1226 $this->initDocument('xml');
1227 $this->elementStart('users', array('type' => 'array',
1228 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1230 if (is_array($user)) {
1231 foreach ($user as $u) {
1232 $twitter_user = $this->twitterUserArray($u);
1233 $this->showTwitterXmlUser($twitter_user);
1236 while ($user->fetch()) {
1237 $twitter_user = $this->twitterUserArray($user);
1238 $this->showTwitterXmlUser($twitter_user);
1242 $this->elementEnd('users');
1243 $this->endDocument('xml');
1246 function showJsonUsers($user)
1248 $this->initDocument('json');
1252 if (is_array($user)) {
1253 foreach ($user as $u) {
1254 $twitter_user = $this->twitterUserArray($u);
1255 array_push($users, $twitter_user);
1258 while ($user->fetch()) {
1259 $twitter_user = $this->twitterUserArray($user);
1260 array_push($users, $twitter_user);
1264 $this->showJsonObjects($users);
1266 $this->endDocument('json');
1269 function showSingleJsonGroup($group)
1271 $this->initDocument('json');
1272 $twitter_group = $this->twitterGroupArray($group);
1273 $this->showJsonObjects($twitter_group);
1274 $this->endDocument('json');
1277 function showSingleXmlGroup($group)
1279 $this->initDocument('xml');
1280 $twitter_group = $this->twitterGroupArray($group);
1281 $this->showTwitterXmlGroup($twitter_group);
1282 $this->endDocument('xml');
1285 function showSingleJsonList($list)
1287 $this->initDocument('json');
1288 $twitter_list = $this->twitterListArray($list);
1289 $this->showJsonObjects($twitter_list);
1290 $this->endDocument('json');
1293 function showSingleXmlList($list)
1295 $this->initDocument('xml');
1296 $twitter_list = $this->twitterListArray($list);
1297 $this->showTwitterXmlList($twitter_list);
1298 $this->endDocument('xml');
1301 function dateTwitter($dt)
1303 $dateStr = date('d F Y H:i:s', strtotime($dt));
1304 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1305 $d->setTimezone(new DateTimeZone(common_timezone()));
1306 return $d->format('D M d H:i:s O Y');
1309 function initDocument($type='xml')
1313 header('Content-Type: application/xml; charset=utf-8');
1317 header('Content-Type: application/json; charset=utf-8');
1319 // Check for JSONP callback
1320 if (isset($this->callback)) {
1321 print $this->callback . '(';
1325 header("Content-Type: application/rss+xml; charset=utf-8");
1326 $this->initTwitterRss();
1329 header('Content-Type: application/atom+xml; charset=utf-8');
1330 $this->initTwitterAtom();
1333 // TRANS: Client error on an API request with an unsupported data format.
1334 $this->clientError(_('Not a supported data format.'));
1340 function endDocument($type='xml')
1347 // Check for JSONP callback
1348 if (isset($this->callback)) {
1353 $this->endTwitterRss();
1356 $this->endTwitterRss();
1359 // TRANS: Client error on an API request with an unsupported data format.
1360 $this->clientError(_('Not a supported data format.'));
1365 function initTwitterRss()
1368 $this->elementStart(
1372 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1373 'xmlns:georss' => 'http://www.georss.org/georss'
1376 $this->elementStart('channel');
1377 Event::handle('StartApiRss', array($this));
1380 function endTwitterRss()
1382 $this->elementEnd('channel');
1383 $this->elementEnd('rss');
1387 function initTwitterAtom()
1390 // FIXME: don't hardcode the language here!
1391 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1392 'xml:lang' => 'en-US',
1393 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1396 function endTwitterAtom()
1398 $this->elementEnd('feed');
1402 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1404 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1405 switch ($content_type) {
1407 $this->showTwitterXmlUser($profile_array);
1410 $this->showJsonObjects($profile_array);
1413 // TRANS: Client error on an API request with an unsupported data format.
1414 $this->clientError(_('Not a supported data format.'));
1419 private static function is_decimal($str)
1421 return preg_match('/^[0-9]+$/', $str);
1424 function getTargetUser($id)
1427 // Twitter supports these other ways of passing the user ID
1428 if (self::is_decimal($this->arg('id'))) {
1429 return User::getKV($this->arg('id'));
1430 } else if ($this->arg('id')) {
1431 $nickname = common_canonical_nickname($this->arg('id'));
1432 return User::getKV('nickname', $nickname);
1433 } else if ($this->arg('user_id')) {
1434 // This is to ensure that a non-numeric user_id still
1435 // overrides screen_name even if it doesn't get used
1436 if (self::is_decimal($this->arg('user_id'))) {
1437 return User::getKV('id', $this->arg('user_id'));
1439 } else if ($this->arg('screen_name')) {
1440 $nickname = common_canonical_nickname($this->arg('screen_name'));
1441 return User::getKV('nickname', $nickname);
1443 // Fall back to trying the currently authenticated user
1444 return $this->auth_user;
1447 } else if (self::is_decimal($id)) {
1448 return User::getKV($id);
1450 $nickname = common_canonical_nickname($id);
1451 return User::getKV('nickname', $nickname);
1455 function getTargetProfile($id)
1459 // Twitter supports these other ways of passing the user ID
1460 if (self::is_decimal($this->arg('id'))) {
1461 return Profile::getKV($this->arg('id'));
1462 } else if ($this->arg('id')) {
1463 // Screen names currently can only uniquely identify a local user.
1464 $nickname = common_canonical_nickname($this->arg('id'));
1465 $user = User::getKV('nickname', $nickname);
1466 return $user ? $user->getProfile() : null;
1467 } else if ($this->arg('user_id')) {
1468 // This is to ensure that a non-numeric user_id still
1469 // overrides screen_name even if it doesn't get used
1470 if (self::is_decimal($this->arg('user_id'))) {
1471 return Profile::getKV('id', $this->arg('user_id'));
1473 } else if ($this->arg('screen_name')) {
1474 $nickname = common_canonical_nickname($this->arg('screen_name'));
1475 $user = User::getKV('nickname', $nickname);
1476 return $user instanceof User ? $user->getProfile() : null;
1478 // Fall back to trying the currently authenticated user
1479 return $this->scoped;
1481 } else if (self::is_decimal($id)) {
1482 return Profile::getKV($id);
1484 $nickname = common_canonical_nickname($id);
1485 $user = User::getKV('nickname', $nickname);
1486 return $user ? $user->getProfile() : null;
1490 function getTargetGroup($id)
1493 if (self::is_decimal($this->arg('id'))) {
1494 return User_group::getKV('id', $this->arg('id'));
1495 } else if ($this->arg('id')) {
1496 return User_group::getForNickname($this->arg('id'));
1497 } else if ($this->arg('group_id')) {
1498 // This is to ensure that a non-numeric group_id still
1499 // overrides group_name even if it doesn't get used
1500 if (self::is_decimal($this->arg('group_id'))) {
1501 return User_group::getKV('id', $this->arg('group_id'));
1503 } else if ($this->arg('group_name')) {
1504 return User_group::getForNickname($this->arg('group_name'));
1507 } else if (self::is_decimal($id)) {
1508 return User_group::getKV('id', $id);
1509 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1510 return User_group::getKV('uri', urldecode($this->arg('uri')));
1512 return User_group::getForNickname($id);
1516 function getTargetList($user=null, $id=null)
1518 $tagger = $this->getTargetUser($user);
1522 $id = $this->arg('id');
1526 if (is_numeric($id)) {
1527 $list = Profile_list::getKV('id', $id);
1529 // only if the list with the id belongs to the tagger
1530 if(empty($list) || $list->tagger != $tagger->id) {
1535 $tag = common_canonical_tag($id);
1536 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1539 if (!empty($list) && $list->private) {
1540 if ($this->auth_user->id == $list->tagger) {
1551 * Returns query argument or default value if not found. Certain
1552 * parameters used throughout the API are lightly scrubbed and
1553 * bounds checked. This overrides Action::arg().
1555 * @param string $key requested argument
1556 * @param string $def default value to return if $key is not provided
1560 function arg($key, $def=null)
1562 // XXX: Do even more input validation/scrubbing?
1564 if (array_key_exists($key, $this->args)) {
1567 $page = (int)$this->args['page'];
1568 return ($page < 1) ? 1 : $page;
1570 $count = (int)$this->args['count'];
1573 } elseif ($count > 200) {
1579 $since_id = (int)$this->args['since_id'];
1580 return ($since_id < 1) ? 0 : $since_id;
1582 $max_id = (int)$this->args['max_id'];
1583 return ($max_id < 1) ? 0 : $max_id;
1585 return parent::arg($key, $def);
1593 * Calculate the complete URI that called up this action. Used for
1594 * Atom rel="self" links. Warning: this is funky.
1596 * @return string URL a URL suitable for rel="self" Atom links
1598 function getSelfUri()
1600 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1602 $id = $this->arg('id');
1603 $aargs = array('format' => $this->format);
1608 $tag = $this->arg('tag');
1610 $aargs['tag'] = $tag;
1613 parse_str($_SERVER['QUERY_STRING'], $params);
1615 if (!empty($params)) {
1616 unset($params['p']);
1617 $pstring = http_build_query($params);
1620 $uri = common_local_url($action, $aargs);
1622 if (!empty($pstring)) {
1623 $uri .= '?' . $pstring;