3 * StatusNet, the distributed open-source microblogging tool
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @author Dan Moore <dan@moore.cx>
26 * @author Evan Prodromou <evan@status.net>
27 * @author Jeffery To <jeffery.to@gmail.com>
28 * @author Toby Inkster <mail@tobyinkster.co.uk>
29 * @author Zach Copley <zach@status.net>
30 * @copyright 2009-2010 StatusNet, Inc.
31 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33 * @link http://status.net/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
77 The URLs for methods referred to in this API documentation are
78 relative to the StatusNet API root. The API root is determined by the
79 site's @b server and @b path variables, which are generally specified
80 in config.php. For example:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
108 * @author Craig Andrews <candrews@integralblue.com>
109 * @author Dan Moore <dan@moore.cx>
110 * @author Evan Prodromou <evan@status.net>
111 * @author Jeffery To <jeffery.to@gmail.com>
112 * @author Toby Inkster <mail@tobyinkster.co.uk>
113 * @author Zach Copley <zach@status.net>
114 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115 * @link http://status.net/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
129 var $callback = null;
131 var $access = self::READ_ONLY; // read (default) or read-write
133 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
138 * @param array $args Web and URL arguments
140 * @return boolean false if user doesn't exist
142 protected function prepare(array $args=array())
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
148 $this->callback = $this->arg('callback');
149 $this->page = (int)$this->arg('page', 1);
150 $this->count = (int)$this->arg('count', 20);
151 $this->max_id = (int)$this->arg('max_id', 0);
152 $this->since_id = (int)$this->arg('since_id', 0);
154 if ($this->arg('since')) {
155 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
158 $this->source = $this->trimmed('source');
160 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
161 $this->source = 'api';
170 * @param array $args Arguments from $_REQUEST
174 protected function handle()
176 header('Access-Control-Allow-Origin: *');
181 * Overrides XMLOutputter::element to write booleans as strings (true|false).
182 * See that method's documentation for more info.
184 * @param string $tag Element type or tagname
185 * @param array $attrs Array of element attributes, as
187 * @param string $content string content of the element
191 function element($tag, $attrs=null, $content=null)
193 if (is_bool($content)) {
194 $content = ($content ? 'true' : 'false');
197 return parent::element($tag, $attrs, $content);
200 function twitterUserArray($profile, $get_notice=false)
202 $twitter_user = array();
205 $user = $profile->getUser();
206 } catch (NoSuchUserException $e) {
210 $twitter_user['id'] = intval($profile->id);
211 $twitter_user['name'] = $profile->getBestName();
212 $twitter_user['screen_name'] = $profile->nickname;
213 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
214 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
216 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
217 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
218 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
220 // START introduced by qvitter API, not necessary for StatusNet API
221 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
223 $avatar = Avatar::getUploaded($profile);
224 $origurl = $avatar->displayUrl();
225 } catch (Exception $e) {
226 $origurl = $twitter_user['profile_image_url_profile_size'];
228 $twitter_user['profile_image_url_original'] = $origurl;
230 $twitter_user['groups_count'] = $profile->getGroupCount();
231 foreach (array('linkcolor', 'backgroundcolor') as $key) {
232 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
234 // END introduced by qvitter API, not necessary for StatusNet API
236 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
237 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
238 $twitter_user['followers_count'] = $profile->subscriberCount();
240 // Note: some profiles don't have an associated user
242 $twitter_user['friends_count'] = $profile->subscriptionCount();
244 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
248 if (!empty($user) && $user->timezone) {
249 $timezone = $user->timezone;
253 $t->setTimezone(new DateTimeZone($timezone));
255 $twitter_user['utc_offset'] = $t->format('Z');
256 $twitter_user['time_zone'] = $timezone;
257 $twitter_user['statuses_count'] = $profile->noticeCount();
259 // Is the requesting user following this user?
260 $twitter_user['following'] = false;
261 $twitter_user['statusnet_blocking'] = false;
262 $twitter_user['notifications'] = false;
264 if (isset($this->auth_user)) {
266 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
267 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
270 $sub = Subscription::pkeyGet(array('subscriber' =>
271 $this->auth_user->id,
272 'subscribed' => $profile->id));
275 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
280 $notice = $profile->getCurrentNotice();
281 if ($notice instanceof Notice) {
283 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
287 // StatusNet-specific
289 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
291 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
292 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
294 return $twitter_user;
297 function twitterStatusArray($notice, $include_user=true)
299 $base = $this->twitterSimpleStatusArray($notice, $include_user);
301 if (!empty($notice->repeat_of)) {
302 $original = Notice::getKV('id', $notice->repeat_of);
303 if ($original instanceof Notice) {
304 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
305 $base['retweeted_status'] = $orig_array;
312 function twitterSimpleStatusArray($notice, $include_user=true)
314 $profile = $notice->getProfile();
316 $twitter_status = array();
317 $twitter_status['text'] = $notice->content;
318 $twitter_status['truncated'] = false; # Not possible on StatusNet
319 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
321 // We could just do $notice->reply_to but maybe the future holds a
322 // different story for parenting.
323 $parent = $notice->getParent();
324 $in_reply_to = $parent->id;
325 } catch (Exception $e) {
328 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
332 $ns = $notice->getSource();
334 if (!empty($ns->name) && !empty($ns->url)) {
335 $source = '<a href="'
336 . htmlspecialchars($ns->url)
337 . '" rel="nofollow">'
338 . htmlspecialchars($ns->name)
345 $twitter_status['uri'] = $notice->getUri();
346 $twitter_status['source'] = $source;
347 $twitter_status['id'] = intval($notice->id);
349 $replier_profile = null;
351 if ($notice->reply_to) {
352 $reply = Notice::getKV(intval($notice->reply_to));
354 $replier_profile = $reply->getProfile();
358 $twitter_status['in_reply_to_user_id'] =
359 ($replier_profile) ? intval($replier_profile->id) : null;
360 $twitter_status['in_reply_to_screen_name'] =
361 ($replier_profile) ? $replier_profile->nickname : null;
363 if (isset($notice->lat) && isset($notice->lon)) {
364 // This is the format that GeoJSON expects stuff to be in
365 $twitter_status['geo'] = array('type' => 'Point',
366 'coordinates' => array((float) $notice->lat,
367 (float) $notice->lon));
369 $twitter_status['geo'] = null;
372 if (!is_null($this->scoped)) {
373 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
375 $twitter_status['repeated'] = false;
379 $attachments = $notice->attachments();
381 if (!empty($attachments)) {
383 $twitter_status['attachments'] = array();
385 foreach ($attachments as $attachment) {
387 $enclosure_o = $attachment->getEnclosure();
388 $enclosure = array();
389 $enclosure['url'] = $enclosure_o->url;
390 $enclosure['mimetype'] = $enclosure_o->mimetype;
391 $enclosure['size'] = $enclosure_o->size;
392 $twitter_status['attachments'][] = $enclosure;
393 } catch (ServerException $e) {
394 // There was not enough metadata available
399 if ($include_user && $profile) {
400 // Don't get notice (recursive!)
401 $twitter_user = $this->twitterUserArray($profile, false);
402 $twitter_status['user'] = $twitter_user;
405 // StatusNet-specific
407 $twitter_status['statusnet_html'] = $notice->rendered;
408 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
410 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
411 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
412 array('include_user'=>$include_user)));
414 return $twitter_status;
417 function twitterGroupArray($group)
419 $twitter_group = array();
421 $twitter_group['id'] = intval($group->id);
422 $twitter_group['url'] = $group->permalink();
423 $twitter_group['nickname'] = $group->nickname;
424 $twitter_group['fullname'] = $group->fullname;
426 if (isset($this->auth_user)) {
427 $twitter_group['member'] = $this->auth_user->isMember($group);
428 $twitter_group['blocked'] = Group_block::isBlocked(
430 $this->auth_user->getProfile()
434 $twitter_group['admin_count'] = $group->getAdminCount();
435 $twitter_group['member_count'] = $group->getMemberCount();
436 $twitter_group['original_logo'] = $group->original_logo;
437 $twitter_group['homepage_logo'] = $group->homepage_logo;
438 $twitter_group['stream_logo'] = $group->stream_logo;
439 $twitter_group['mini_logo'] = $group->mini_logo;
440 $twitter_group['homepage'] = $group->homepage;
441 $twitter_group['description'] = $group->description;
442 $twitter_group['location'] = $group->location;
443 $twitter_group['created'] = $this->dateTwitter($group->created);
444 $twitter_group['modified'] = $this->dateTwitter($group->modified);
446 return $twitter_group;
449 function twitterRssGroupArray($group)
452 $entry['content']=$group->description;
453 $entry['title']=$group->nickname;
454 $entry['link']=$group->permalink();
455 $entry['published']=common_date_iso8601($group->created);
456 $entry['updated']==common_date_iso8601($group->modified);
457 $taguribase = common_config('integration', 'groupuri');
458 $entry['id'] = "group:$groupuribase:$entry[link]";
460 $entry['description'] = $entry['content'];
461 $entry['pubDate'] = common_date_rfc2822($group->created);
462 $entry['guid'] = $entry['link'];
467 function twitterListArray($list)
469 $profile = Profile::getKV('id', $list->tagger);
471 $twitter_list = array();
472 $twitter_list['id'] = $list->id;
473 $twitter_list['name'] = $list->tag;
474 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
475 $twitter_list['slug'] = $list->tag;
476 $twitter_list['description'] = $list->description;
477 $twitter_list['subscriber_count'] = $list->subscriberCount();
478 $twitter_list['member_count'] = $list->taggedCount();
479 $twitter_list['uri'] = $list->getUri();
481 if (isset($this->auth_user)) {
482 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
484 $twitter_list['following'] = false;
487 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
488 $twitter_list['user'] = $this->twitterUserArray($profile, false);
490 return $twitter_list;
493 function twitterRssEntryArray($notice)
497 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
498 $profile = $notice->getProfile();
500 // We trim() to avoid extraneous whitespace in the output
502 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
503 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
504 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
505 $entry['published'] = common_date_iso8601($notice->created);
507 $taguribase = TagURI::base();
508 $entry['id'] = "tag:$taguribase:$entry[link]";
510 $entry['updated'] = $entry['published'];
511 $entry['author'] = $profile->getBestName();
514 $attachments = $notice->attachments();
515 $enclosures = array();
517 foreach ($attachments as $attachment) {
519 $enclosure_o = $attachment->getEnclosure();
520 $enclosure = array();
521 $enclosure['url'] = $enclosure_o->url;
522 $enclosure['mimetype'] = $enclosure_o->mimetype;
523 $enclosure['size'] = $enclosure_o->size;
524 $enclosures[] = $enclosure;
525 } catch (ServerException $e) {
526 // There was not enough metadata available
530 if (!empty($enclosures)) {
531 $entry['enclosures'] = $enclosures;
535 $tag = new Notice_tag();
536 $tag->notice_id = $notice->id;
538 $entry['tags']=array();
539 while ($tag->fetch()) {
540 $entry['tags'][]=$tag->tag;
546 $entry['description'] = $entry['content'];
547 $entry['pubDate'] = common_date_rfc2822($notice->created);
548 $entry['guid'] = $entry['link'];
550 if (isset($notice->lat) && isset($notice->lon)) {
551 // This is the format that GeoJSON expects stuff to be in.
552 // showGeoRSS() below uses it for XML output, so we reuse it
553 $entry['geo'] = array('type' => 'Point',
554 'coordinates' => array((float) $notice->lat,
555 (float) $notice->lon));
557 $entry['geo'] = null;
560 Event::handle('EndRssEntryArray', array($notice, &$entry));
566 function twitterRelationshipArray($source, $target)
568 $relationship = array();
570 $relationship['source'] =
571 $this->relationshipDetailsArray($source, $target);
572 $relationship['target'] =
573 $this->relationshipDetailsArray($target, $source);
575 return array('relationship' => $relationship);
578 function relationshipDetailsArray($source, $target)
582 $details['screen_name'] = $source->nickname;
583 $details['followed_by'] = $target->isSubscribed($source);
584 $details['following'] = $source->isSubscribed($target);
586 $notifications = false;
588 if ($source->isSubscribed($target)) {
589 $sub = Subscription::pkeyGet(array('subscriber' =>
590 $source->id, 'subscribed' => $target->id));
593 $notifications = ($sub->jabber || $sub->sms);
597 $details['notifications_enabled'] = $notifications;
598 $details['blocking'] = $source->hasBlocked($target);
599 $details['id'] = intval($source->id);
604 function showTwitterXmlRelationship($relationship)
606 $this->elementStart('relationship');
608 foreach($relationship as $element => $value) {
609 if ($element == 'source' || $element == 'target') {
610 $this->elementStart($element);
611 $this->showXmlRelationshipDetails($value);
612 $this->elementEnd($element);
616 $this->elementEnd('relationship');
619 function showXmlRelationshipDetails($details)
621 foreach($details as $element => $value) {
622 $this->element($element, null, $value);
626 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
630 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
632 $this->elementStart($tag, $attrs);
633 foreach($twitter_status as $element => $value) {
636 $this->showTwitterXmlUser($twitter_status['user']);
639 $this->element($element, null, common_xml_safe_str($value));
642 $this->showXmlAttachments($twitter_status['attachments']);
645 $this->showGeoXML($value);
647 case 'retweeted_status':
648 $this->showTwitterXmlStatus($value, 'retweeted_status');
651 if (strncmp($element, 'statusnet_', 10) == 0) {
652 $this->element('statusnet:'.substr($element, 10), null, $value);
654 $this->element($element, null, $value);
658 $this->elementEnd($tag);
661 function showTwitterXmlGroup($twitter_group)
663 $this->elementStart('group');
664 foreach($twitter_group as $element => $value) {
665 $this->element($element, null, $value);
667 $this->elementEnd('group');
670 function showTwitterXmlList($twitter_list)
672 $this->elementStart('list');
673 foreach($twitter_list as $element => $value) {
674 if($element == 'user') {
675 $this->showTwitterXmlUser($value, 'user');
678 $this->element($element, null, $value);
681 $this->elementEnd('list');
684 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
688 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
690 $this->elementStart($role, $attrs);
691 foreach($twitter_user as $element => $value) {
692 if ($element == 'status') {
693 $this->showTwitterXmlStatus($twitter_user['status']);
694 } else if (strncmp($element, 'statusnet_', 10) == 0) {
695 $this->element('statusnet:'.substr($element, 10), null, $value);
697 $this->element($element, null, $value);
700 $this->elementEnd($role);
703 function showXmlAttachments($attachments) {
704 if (!empty($attachments)) {
705 $this->elementStart('attachments', array('type' => 'array'));
706 foreach ($attachments as $attachment) {
708 $attrs['url'] = $attachment['url'];
709 $attrs['mimetype'] = $attachment['mimetype'];
710 $attrs['size'] = $attachment['size'];
711 $this->element('enclosure', $attrs, '');
713 $this->elementEnd('attachments');
717 function showGeoXML($geo)
721 $this->element('geo');
723 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
724 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
725 $this->elementEnd('geo');
729 function showGeoRSS($geo)
735 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
740 function showTwitterRssItem($entry)
742 $this->elementStart('item');
743 $this->element('title', null, $entry['title']);
744 $this->element('description', null, $entry['description']);
745 $this->element('pubDate', null, $entry['pubDate']);
746 $this->element('guid', null, $entry['guid']);
747 $this->element('link', null, $entry['link']);
749 // RSS only supports 1 enclosure per item
750 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
751 $enclosure = $entry['enclosures'][0];
752 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
755 if(array_key_exists('tags', $entry)){
756 foreach($entry['tags'] as $tag){
757 $this->element('category', null,$tag);
761 $this->showGeoRSS($entry['geo']);
762 $this->elementEnd('item');
765 function showJsonObjects($objects)
767 print(json_encode($objects));
770 function showSingleXmlStatus($notice)
772 $this->initDocument('xml');
773 $twitter_status = $this->twitterStatusArray($notice);
774 $this->showTwitterXmlStatus($twitter_status, 'status', true);
775 $this->endDocument('xml');
778 function showSingleAtomStatus($notice)
780 header('Content-Type: application/atom+xml; charset=utf-8');
781 print $notice->asAtomEntry(true, true, true, $this->auth_user);
784 function show_single_json_status($notice)
786 $this->initDocument('json');
787 $status = $this->twitterStatusArray($notice);
788 $this->showJsonObjects($status);
789 $this->endDocument('json');
792 function showXmlTimeline($notice)
794 $this->initDocument('xml');
795 $this->elementStart('statuses', array('type' => 'array',
796 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
798 if (is_array($notice)) {
799 $notice = new ArrayWrapper($notice);
802 while ($notice->fetch()) {
804 $twitter_status = $this->twitterStatusArray($notice);
805 $this->showTwitterXmlStatus($twitter_status);
806 } catch (Exception $e) {
807 common_log(LOG_ERR, $e->getMessage());
812 $this->elementEnd('statuses');
813 $this->endDocument('xml');
816 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
818 $this->initDocument('rss');
820 $this->element('title', null, $title);
821 $this->element('link', null, $link);
823 if (!is_null($self)) {
827 'type' => 'application/rss+xml',
834 if (!is_null($suplink)) {
835 // For FriendFeed's SUP protocol
836 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
837 'rel' => 'http://api.friendfeed.com/2008/03#sup',
839 'type' => 'application/json'));
842 if (!is_null($logo)) {
843 $this->elementStart('image');
844 $this->element('link', null, $link);
845 $this->element('title', null, $title);
846 $this->element('url', null, $logo);
847 $this->elementEnd('image');
850 $this->element('description', null, $subtitle);
851 $this->element('language', null, 'en-us');
852 $this->element('ttl', null, '40');
854 if (is_array($notice)) {
855 $notice = new ArrayWrapper($notice);
858 while ($notice->fetch()) {
860 $entry = $this->twitterRssEntryArray($notice);
861 $this->showTwitterRssItem($entry);
862 } catch (Exception $e) {
863 common_log(LOG_ERR, $e->getMessage());
864 // continue on exceptions
868 $this->endTwitterRss();
871 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
873 $this->initDocument('atom');
875 $this->element('title', null, $title);
876 $this->element('id', null, $id);
877 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
879 if (!is_null($logo)) {
880 $this->element('logo',null,$logo);
883 if (!is_null($suplink)) {
884 // For FriendFeed's SUP protocol
885 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
887 'type' => 'application/json'));
890 if (!is_null($selfuri)) {
891 $this->element('link', array('href' => $selfuri,
892 'rel' => 'self', 'type' => 'application/atom+xml'), null);
895 $this->element('updated', null, common_date_iso8601('now'));
896 $this->element('subtitle', null, $subtitle);
898 if (is_array($notice)) {
899 $notice = new ArrayWrapper($notice);
902 while ($notice->fetch()) {
904 $this->raw($notice->asAtomEntry());
905 } catch (Exception $e) {
906 common_log(LOG_ERR, $e->getMessage());
911 $this->endDocument('atom');
914 function showRssGroups($group, $title, $link, $subtitle)
916 $this->initDocument('rss');
918 $this->element('title', null, $title);
919 $this->element('link', null, $link);
920 $this->element('description', null, $subtitle);
921 $this->element('language', null, 'en-us');
922 $this->element('ttl', null, '40');
924 if (is_array($group)) {
925 foreach ($group as $g) {
926 $twitter_group = $this->twitterRssGroupArray($g);
927 $this->showTwitterRssItem($twitter_group);
930 while ($group->fetch()) {
931 $twitter_group = $this->twitterRssGroupArray($group);
932 $this->showTwitterRssItem($twitter_group);
936 $this->endTwitterRss();
939 function showTwitterAtomEntry($entry)
941 $this->elementStart('entry');
942 $this->element('title', null, common_xml_safe_str($entry['title']));
945 array('type' => 'html'),
946 common_xml_safe_str($entry['content'])
948 $this->element('id', null, $entry['id']);
949 $this->element('published', null, $entry['published']);
950 $this->element('updated', null, $entry['updated']);
951 $this->element('link', array('type' => 'text/html',
952 'href' => $entry['link'],
953 'rel' => 'alternate'));
954 $this->element('link', array('type' => $entry['avatar-type'],
955 'href' => $entry['avatar'],
957 $this->elementStart('author');
959 $this->element('name', null, $entry['author-name']);
960 $this->element('uri', null, $entry['author-uri']);
962 $this->elementEnd('author');
963 $this->elementEnd('entry');
966 function showXmlDirectMessage($dm, $namespaces=false)
970 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
972 $this->elementStart('direct_message', $attrs);
973 foreach($dm as $element => $value) {
977 $this->showTwitterXmlUser($value, $element);
980 $this->element($element, null, common_xml_safe_str($value));
983 $this->element($element, null, $value);
987 $this->elementEnd('direct_message');
990 function directMessageArray($message)
994 $from_profile = $message->getFrom();
995 $to_profile = $message->getTo();
997 $dmsg['id'] = intval($message->id);
998 $dmsg['sender_id'] = intval($from_profile->id);
999 $dmsg['text'] = trim($message->content);
1000 $dmsg['recipient_id'] = intval($to_profile->id);
1001 $dmsg['created_at'] = $this->dateTwitter($message->created);
1002 $dmsg['sender_screen_name'] = $from_profile->nickname;
1003 $dmsg['recipient_screen_name'] = $to_profile->nickname;
1004 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
1005 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1010 function rssDirectMessageArray($message)
1014 $from = $message->getFrom();
1016 $entry['title'] = sprintf('Message from %1$s to %2$s',
1017 $from->nickname, $message->getTo()->nickname);
1019 $entry['content'] = common_xml_safe_str($message->rendered);
1020 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1021 $entry['published'] = common_date_iso8601($message->created);
1023 $taguribase = TagURI::base();
1025 $entry['id'] = "tag:$taguribase:$entry[link]";
1026 $entry['updated'] = $entry['published'];
1028 $entry['author-name'] = $from->getBestName();
1029 $entry['author-uri'] = $from->homepage;
1031 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1033 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1034 $entry['avatar-type'] = $avatar->mediatype;
1035 } catch (Exception $e) {
1036 $entry['avatar-type'] = 'image/png';
1039 // RSS item specific
1041 $entry['description'] = $entry['content'];
1042 $entry['pubDate'] = common_date_rfc2822($message->created);
1043 $entry['guid'] = $entry['link'];
1048 function showSingleXmlDirectMessage($message)
1050 $this->initDocument('xml');
1051 $dmsg = $this->directMessageArray($message);
1052 $this->showXmlDirectMessage($dmsg, true);
1053 $this->endDocument('xml');
1056 function showSingleJsonDirectMessage($message)
1058 $this->initDocument('json');
1059 $dmsg = $this->directMessageArray($message);
1060 $this->showJsonObjects($dmsg);
1061 $this->endDocument('json');
1064 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1066 $this->initDocument('atom');
1068 $this->element('title', null, common_xml_safe_str($title));
1069 $this->element('id', null, $id);
1070 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1072 if (!is_null($selfuri)) {
1073 $this->element('link', array('href' => $selfuri,
1074 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1077 $this->element('updated', null, common_date_iso8601('now'));
1078 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1080 if (is_array($group)) {
1081 foreach ($group as $g) {
1082 $this->raw($g->asAtomEntry());
1085 while ($group->fetch()) {
1086 $this->raw($group->asAtomEntry());
1090 $this->endDocument('atom');
1094 function showJsonTimeline($notice)
1096 $this->initDocument('json');
1098 $statuses = array();
1100 if (is_array($notice)) {
1101 $notice = new ArrayWrapper($notice);
1104 while ($notice->fetch()) {
1106 $twitter_status = $this->twitterStatusArray($notice);
1107 array_push($statuses, $twitter_status);
1108 } catch (Exception $e) {
1109 common_log(LOG_ERR, $e->getMessage());
1114 $this->showJsonObjects($statuses);
1116 $this->endDocument('json');
1119 function showJsonGroups($group)
1121 $this->initDocument('json');
1125 if (is_array($group)) {
1126 foreach ($group as $g) {
1127 $twitter_group = $this->twitterGroupArray($g);
1128 array_push($groups, $twitter_group);
1131 while ($group->fetch()) {
1132 $twitter_group = $this->twitterGroupArray($group);
1133 array_push($groups, $twitter_group);
1137 $this->showJsonObjects($groups);
1139 $this->endDocument('json');
1142 function showXmlGroups($group)
1145 $this->initDocument('xml');
1146 $this->elementStart('groups', array('type' => 'array'));
1148 if (is_array($group)) {
1149 foreach ($group as $g) {
1150 $twitter_group = $this->twitterGroupArray($g);
1151 $this->showTwitterXmlGroup($twitter_group);
1154 while ($group->fetch()) {
1155 $twitter_group = $this->twitterGroupArray($group);
1156 $this->showTwitterXmlGroup($twitter_group);
1160 $this->elementEnd('groups');
1161 $this->endDocument('xml');
1164 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1167 $this->initDocument('xml');
1168 $this->elementStart('lists_list');
1169 $this->elementStart('lists', array('type' => 'array'));
1171 if (is_array($list)) {
1172 foreach ($list as $l) {
1173 $twitter_list = $this->twitterListArray($l);
1174 $this->showTwitterXmlList($twitter_list);
1177 while ($list->fetch()) {
1178 $twitter_list = $this->twitterListArray($list);
1179 $this->showTwitterXmlList($twitter_list);
1183 $this->elementEnd('lists');
1185 $this->element('next_cursor', null, $next_cursor);
1186 $this->element('previous_cursor', null, $prev_cursor);
1188 $this->elementEnd('lists_list');
1189 $this->endDocument('xml');
1192 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1194 $this->initDocument('json');
1198 if (is_array($list)) {
1199 foreach ($list as $l) {
1200 $twitter_list = $this->twitterListArray($l);
1201 array_push($lists, $twitter_list);
1204 while ($list->fetch()) {
1205 $twitter_list = $this->twitterListArray($list);
1206 array_push($lists, $twitter_list);
1210 $lists_list = array(
1212 'next_cursor' => $next_cursor,
1213 'next_cursor_str' => strval($next_cursor),
1214 'previous_cursor' => $prev_cursor,
1215 'previous_cursor_str' => strval($prev_cursor)
1218 $this->showJsonObjects($lists_list);
1220 $this->endDocument('json');
1223 function showTwitterXmlUsers($user)
1225 $this->initDocument('xml');
1226 $this->elementStart('users', array('type' => 'array',
1227 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1229 if (is_array($user)) {
1230 foreach ($user as $u) {
1231 $twitter_user = $this->twitterUserArray($u);
1232 $this->showTwitterXmlUser($twitter_user);
1235 while ($user->fetch()) {
1236 $twitter_user = $this->twitterUserArray($user);
1237 $this->showTwitterXmlUser($twitter_user);
1241 $this->elementEnd('users');
1242 $this->endDocument('xml');
1245 function showJsonUsers($user)
1247 $this->initDocument('json');
1251 if (is_array($user)) {
1252 foreach ($user as $u) {
1253 $twitter_user = $this->twitterUserArray($u);
1254 array_push($users, $twitter_user);
1257 while ($user->fetch()) {
1258 $twitter_user = $this->twitterUserArray($user);
1259 array_push($users, $twitter_user);
1263 $this->showJsonObjects($users);
1265 $this->endDocument('json');
1268 function showSingleJsonGroup($group)
1270 $this->initDocument('json');
1271 $twitter_group = $this->twitterGroupArray($group);
1272 $this->showJsonObjects($twitter_group);
1273 $this->endDocument('json');
1276 function showSingleXmlGroup($group)
1278 $this->initDocument('xml');
1279 $twitter_group = $this->twitterGroupArray($group);
1280 $this->showTwitterXmlGroup($twitter_group);
1281 $this->endDocument('xml');
1284 function showSingleJsonList($list)
1286 $this->initDocument('json');
1287 $twitter_list = $this->twitterListArray($list);
1288 $this->showJsonObjects($twitter_list);
1289 $this->endDocument('json');
1292 function showSingleXmlList($list)
1294 $this->initDocument('xml');
1295 $twitter_list = $this->twitterListArray($list);
1296 $this->showTwitterXmlList($twitter_list);
1297 $this->endDocument('xml');
1300 function dateTwitter($dt)
1302 $dateStr = date('d F Y H:i:s', strtotime($dt));
1303 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1304 $d->setTimezone(new DateTimeZone(common_timezone()));
1305 return $d->format('D M d H:i:s O Y');
1308 function initDocument($type='xml')
1312 header('Content-Type: application/xml; charset=utf-8');
1316 header('Content-Type: application/json; charset=utf-8');
1318 // Check for JSONP callback
1319 if (isset($this->callback)) {
1320 print $this->callback . '(';
1324 header("Content-Type: application/rss+xml; charset=utf-8");
1325 $this->initTwitterRss();
1328 header('Content-Type: application/atom+xml; charset=utf-8');
1329 $this->initTwitterAtom();
1332 // TRANS: Client error on an API request with an unsupported data format.
1333 $this->clientError(_('Not a supported data format.'));
1339 function endDocument($type='xml')
1346 // Check for JSONP callback
1347 if (isset($this->callback)) {
1352 $this->endTwitterRss();
1355 $this->endTwitterRss();
1358 // TRANS: Client error on an API request with an unsupported data format.
1359 $this->clientError(_('Not a supported data format.'));
1364 function initTwitterRss()
1367 $this->elementStart(
1371 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1372 'xmlns:georss' => 'http://www.georss.org/georss'
1375 $this->elementStart('channel');
1376 Event::handle('StartApiRss', array($this));
1379 function endTwitterRss()
1381 $this->elementEnd('channel');
1382 $this->elementEnd('rss');
1386 function initTwitterAtom()
1389 // FIXME: don't hardcode the language here!
1390 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1391 'xml:lang' => 'en-US',
1392 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1395 function endTwitterAtom()
1397 $this->elementEnd('feed');
1401 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1403 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1404 switch ($content_type) {
1406 $this->showTwitterXmlUser($profile_array);
1409 $this->showJsonObjects($profile_array);
1412 // TRANS: Client error on an API request with an unsupported data format.
1413 $this->clientError(_('Not a supported data format.'));
1418 private static function is_decimal($str)
1420 return preg_match('/^[0-9]+$/', $str);
1423 function getTargetUser($id)
1426 // Twitter supports these other ways of passing the user ID
1427 if (self::is_decimal($this->arg('id'))) {
1428 return User::getKV($this->arg('id'));
1429 } else if ($this->arg('id')) {
1430 $nickname = common_canonical_nickname($this->arg('id'));
1431 return User::getKV('nickname', $nickname);
1432 } else if ($this->arg('user_id')) {
1433 // This is to ensure that a non-numeric user_id still
1434 // overrides screen_name even if it doesn't get used
1435 if (self::is_decimal($this->arg('user_id'))) {
1436 return User::getKV('id', $this->arg('user_id'));
1438 } else if ($this->arg('screen_name')) {
1439 $nickname = common_canonical_nickname($this->arg('screen_name'));
1440 return User::getKV('nickname', $nickname);
1442 // Fall back to trying the currently authenticated user
1443 return $this->auth_user;
1446 } else if (self::is_decimal($id)) {
1447 return User::getKV($id);
1449 $nickname = common_canonical_nickname($id);
1450 return User::getKV('nickname', $nickname);
1454 function getTargetProfile($id)
1458 // Twitter supports these other ways of passing the user ID
1459 if (self::is_decimal($this->arg('id'))) {
1460 return Profile::getKV($this->arg('id'));
1461 } else if ($this->arg('id')) {
1462 // Screen names currently can only uniquely identify a local user.
1463 $nickname = common_canonical_nickname($this->arg('id'));
1464 $user = User::getKV('nickname', $nickname);
1465 return $user ? $user->getProfile() : null;
1466 } else if ($this->arg('user_id')) {
1467 // This is to ensure that a non-numeric user_id still
1468 // overrides screen_name even if it doesn't get used
1469 if (self::is_decimal($this->arg('user_id'))) {
1470 return Profile::getKV('id', $this->arg('user_id'));
1472 } else if ($this->arg('screen_name')) {
1473 $nickname = common_canonical_nickname($this->arg('screen_name'));
1474 $user = User::getKV('nickname', $nickname);
1475 return $user instanceof User ? $user->getProfile() : null;
1477 // Fall back to trying the currently authenticated user
1478 return $this->scoped;
1480 } else if (self::is_decimal($id)) {
1481 return Profile::getKV($id);
1483 $nickname = common_canonical_nickname($id);
1484 $user = User::getKV('nickname', $nickname);
1485 return $user ? $user->getProfile() : null;
1489 function getTargetGroup($id)
1492 if (self::is_decimal($this->arg('id'))) {
1493 return User_group::getKV('id', $this->arg('id'));
1494 } else if ($this->arg('id')) {
1495 return User_group::getForNickname($this->arg('id'));
1496 } else if ($this->arg('group_id')) {
1497 // This is to ensure that a non-numeric group_id still
1498 // overrides group_name even if it doesn't get used
1499 if (self::is_decimal($this->arg('group_id'))) {
1500 return User_group::getKV('id', $this->arg('group_id'));
1502 } else if ($this->arg('group_name')) {
1503 return User_group::getForNickname($this->arg('group_name'));
1506 } else if (self::is_decimal($id)) {
1507 return User_group::getKV('id', $id);
1508 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1509 return User_group::getKV('uri', urldecode($this->arg('uri')));
1511 return User_group::getForNickname($id);
1515 function getTargetList($user=null, $id=null)
1517 $tagger = $this->getTargetUser($user);
1521 $id = $this->arg('id');
1525 if (is_numeric($id)) {
1526 $list = Profile_list::getKV('id', $id);
1528 // only if the list with the id belongs to the tagger
1529 if(empty($list) || $list->tagger != $tagger->id) {
1534 $tag = common_canonical_tag($id);
1535 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1538 if (!empty($list) && $list->private) {
1539 if ($this->auth_user->id == $list->tagger) {
1550 * Returns query argument or default value if not found. Certain
1551 * parameters used throughout the API are lightly scrubbed and
1552 * bounds checked. This overrides Action::arg().
1554 * @param string $key requested argument
1555 * @param string $def default value to return if $key is not provided
1559 function arg($key, $def=null)
1561 // XXX: Do even more input validation/scrubbing?
1563 if (array_key_exists($key, $this->args)) {
1566 $page = (int)$this->args['page'];
1567 return ($page < 1) ? 1 : $page;
1569 $count = (int)$this->args['count'];
1572 } elseif ($count > 200) {
1578 $since_id = (int)$this->args['since_id'];
1579 return ($since_id < 1) ? 0 : $since_id;
1581 $max_id = (int)$this->args['max_id'];
1582 return ($max_id < 1) ? 0 : $max_id;
1584 return parent::arg($key, $def);
1592 * Calculate the complete URI that called up this action. Used for
1593 * Atom rel="self" links. Warning: this is funky.
1595 * @return string URL a URL suitable for rel="self" Atom links
1597 function getSelfUri()
1599 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1601 $id = $this->arg('id');
1602 $aargs = array('format' => $this->format);
1607 $tag = $this->arg('tag');
1609 $aargs['tag'] = $tag;
1612 parse_str($_SERVER['QUERY_STRING'], $params);
1614 if (!empty($params)) {
1615 unset($params['p']);
1616 $pstring = http_build_query($params);
1619 $uri = common_local_url($action, $aargs);
1621 if (!empty($pstring)) {
1622 $uri .= '?' . $pstring;