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);
246 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
250 if (!empty($user) && $user->timezone) {
251 $timezone = $user->timezone;
255 $t->setTimezone(new DateTimeZone($timezone));
257 $twitter_user['utc_offset'] = $t->format('Z');
258 $twitter_user['time_zone'] = $timezone;
259 $twitter_user['statuses_count'] = $profile->noticeCount();
261 // Is the requesting user following this user?
262 $twitter_user['following'] = false;
263 $twitter_user['statusnet_blocking'] = false;
264 $twitter_user['notifications'] = false;
266 if (isset($this->auth_user)) {
268 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
269 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
272 $sub = Subscription::pkeyGet(array('subscriber' =>
273 $this->auth_user->id,
274 'subscribed' => $profile->id));
277 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
282 $notice = $profile->getCurrentNotice();
283 if ($notice instanceof Notice) {
285 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
289 // StatusNet-specific
291 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
293 return $twitter_user;
296 function twitterStatusArray($notice, $include_user=true)
298 $base = $this->twitterSimpleStatusArray($notice, $include_user);
300 if (!empty($notice->repeat_of)) {
301 $original = Notice::getKV('id', $notice->repeat_of);
302 if (!empty($original)) {
303 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
304 $base['retweeted_status'] = $original_array;
311 function twitterSimpleStatusArray($notice, $include_user=true)
313 $profile = $notice->getProfile();
315 $twitter_status = array();
316 $twitter_status['text'] = $notice->content;
317 $twitter_status['truncated'] = false; # Not possible on StatusNet
318 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
320 // We could just do $notice->reply_to but maybe the future holds a
321 // different story for parenting.
322 $parent = $notice->getParent();
323 $in_reply_to = $parent->id;
324 } catch (Exception $e) {
327 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
331 $ns = $notice->getSource();
333 if (!empty($ns->name) && !empty($ns->url)) {
334 $source = '<a href="'
335 . htmlspecialchars($ns->url)
336 . '" rel="nofollow">'
337 . htmlspecialchars($ns->name)
344 $twitter_status['uri'] = $notice->getUri();
345 $twitter_status['source'] = $source;
346 $twitter_status['id'] = intval($notice->id);
348 $replier_profile = null;
350 if ($notice->reply_to) {
351 $reply = Notice::getKV(intval($notice->reply_to));
353 $replier_profile = $reply->getProfile();
357 $twitter_status['in_reply_to_user_id'] =
358 ($replier_profile) ? intval($replier_profile->id) : null;
359 $twitter_status['in_reply_to_screen_name'] =
360 ($replier_profile) ? $replier_profile->nickname : null;
362 if (isset($notice->lat) && isset($notice->lon)) {
363 // This is the format that GeoJSON expects stuff to be in
364 $twitter_status['geo'] = array('type' => 'Point',
365 'coordinates' => array((float) $notice->lat,
366 (float) $notice->lon));
368 $twitter_status['geo'] = null;
371 if (!is_null($this->scoped)) {
372 $twitter_status['favorited'] = $this->scoped->hasFave($notice);
373 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
375 $twitter_status['favorited'] = false;
376 $twitter_status['repeated'] = false;
380 $attachments = $notice->attachments();
382 if (!empty($attachments)) {
384 $twitter_status['attachments'] = array();
386 foreach ($attachments as $attachment) {
387 $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;
398 if ($include_user && $profile) {
399 // Don't get notice (recursive!)
400 $twitter_user = $this->twitterUserArray($profile, false);
401 $twitter_status['user'] = $twitter_user;
404 // StatusNet-specific
406 $twitter_status['statusnet_html'] = $notice->rendered;
407 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
409 return $twitter_status;
412 function twitterGroupArray($group)
414 $twitter_group = array();
416 $twitter_group['id'] = intval($group->id);
417 $twitter_group['url'] = $group->permalink();
418 $twitter_group['nickname'] = $group->nickname;
419 $twitter_group['fullname'] = $group->fullname;
421 if (isset($this->auth_user)) {
422 $twitter_group['member'] = $this->auth_user->isMember($group);
423 $twitter_group['blocked'] = Group_block::isBlocked(
425 $this->auth_user->getProfile()
429 $twitter_group['admin_count'] = $group->getAdminCount();
430 $twitter_group['member_count'] = $group->getMemberCount();
431 $twitter_group['original_logo'] = $group->original_logo;
432 $twitter_group['homepage_logo'] = $group->homepage_logo;
433 $twitter_group['stream_logo'] = $group->stream_logo;
434 $twitter_group['mini_logo'] = $group->mini_logo;
435 $twitter_group['homepage'] = $group->homepage;
436 $twitter_group['description'] = $group->description;
437 $twitter_group['location'] = $group->location;
438 $twitter_group['created'] = $this->dateTwitter($group->created);
439 $twitter_group['modified'] = $this->dateTwitter($group->modified);
441 return $twitter_group;
444 function twitterRssGroupArray($group)
447 $entry['content']=$group->description;
448 $entry['title']=$group->nickname;
449 $entry['link']=$group->permalink();
450 $entry['published']=common_date_iso8601($group->created);
451 $entry['updated']==common_date_iso8601($group->modified);
452 $taguribase = common_config('integration', 'groupuri');
453 $entry['id'] = "group:$groupuribase:$entry[link]";
455 $entry['description'] = $entry['content'];
456 $entry['pubDate'] = common_date_rfc2822($group->created);
457 $entry['guid'] = $entry['link'];
462 function twitterListArray($list)
464 $profile = Profile::getKV('id', $list->tagger);
466 $twitter_list = array();
467 $twitter_list['id'] = $list->id;
468 $twitter_list['name'] = $list->tag;
469 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
470 $twitter_list['slug'] = $list->tag;
471 $twitter_list['description'] = $list->description;
472 $twitter_list['subscriber_count'] = $list->subscriberCount();
473 $twitter_list['member_count'] = $list->taggedCount();
474 $twitter_list['uri'] = $list->getUri();
476 if (isset($this->auth_user)) {
477 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
479 $twitter_list['following'] = false;
482 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
483 $twitter_list['user'] = $this->twitterUserArray($profile, false);
485 return $twitter_list;
488 function twitterRssEntryArray($notice)
492 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
493 $profile = $notice->getProfile();
495 // We trim() to avoid extraneous whitespace in the output
497 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
498 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
499 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
500 $entry['published'] = common_date_iso8601($notice->created);
502 $taguribase = TagURI::base();
503 $entry['id'] = "tag:$taguribase:$entry[link]";
505 $entry['updated'] = $entry['published'];
506 $entry['author'] = $profile->getBestName();
509 $attachments = $notice->attachments();
510 $enclosures = array();
512 foreach ($attachments as $attachment) {
513 $enclosure_o=$attachment->getEnclosure();
515 $enclosure = array();
516 $enclosure['url'] = $enclosure_o->url;
517 $enclosure['mimetype'] = $enclosure_o->mimetype;
518 $enclosure['size'] = $enclosure_o->size;
519 $enclosures[] = $enclosure;
523 if (!empty($enclosures)) {
524 $entry['enclosures'] = $enclosures;
528 $tag = new Notice_tag();
529 $tag->notice_id = $notice->id;
531 $entry['tags']=array();
532 while ($tag->fetch()) {
533 $entry['tags'][]=$tag->tag;
539 $entry['description'] = $entry['content'];
540 $entry['pubDate'] = common_date_rfc2822($notice->created);
541 $entry['guid'] = $entry['link'];
543 if (isset($notice->lat) && isset($notice->lon)) {
544 // This is the format that GeoJSON expects stuff to be in.
545 // showGeoRSS() below uses it for XML output, so we reuse it
546 $entry['geo'] = array('type' => 'Point',
547 'coordinates' => array((float) $notice->lat,
548 (float) $notice->lon));
550 $entry['geo'] = null;
553 Event::handle('EndRssEntryArray', array($notice, &$entry));
559 function twitterRelationshipArray($source, $target)
561 $relationship = array();
563 $relationship['source'] =
564 $this->relationshipDetailsArray($source, $target);
565 $relationship['target'] =
566 $this->relationshipDetailsArray($target, $source);
568 return array('relationship' => $relationship);
571 function relationshipDetailsArray($source, $target)
575 $details['screen_name'] = $source->nickname;
576 $details['followed_by'] = $target->isSubscribed($source);
577 $details['following'] = $source->isSubscribed($target);
579 $notifications = false;
581 if ($source->isSubscribed($target)) {
582 $sub = Subscription::pkeyGet(array('subscriber' =>
583 $source->id, 'subscribed' => $target->id));
586 $notifications = ($sub->jabber || $sub->sms);
590 $details['notifications_enabled'] = $notifications;
591 $details['blocking'] = $source->hasBlocked($target);
592 $details['id'] = intval($source->id);
597 function showTwitterXmlRelationship($relationship)
599 $this->elementStart('relationship');
601 foreach($relationship as $element => $value) {
602 if ($element == 'source' || $element == 'target') {
603 $this->elementStart($element);
604 $this->showXmlRelationshipDetails($value);
605 $this->elementEnd($element);
609 $this->elementEnd('relationship');
612 function showXmlRelationshipDetails($details)
614 foreach($details as $element => $value) {
615 $this->element($element, null, $value);
619 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
623 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
625 $this->elementStart($tag, $attrs);
626 foreach($twitter_status as $element => $value) {
629 $this->showTwitterXmlUser($twitter_status['user']);
632 $this->element($element, null, common_xml_safe_str($value));
635 $this->showXmlAttachments($twitter_status['attachments']);
638 $this->showGeoXML($value);
640 case 'retweeted_status':
641 $this->showTwitterXmlStatus($value, 'retweeted_status');
644 if (strncmp($element, 'statusnet_', 10) == 0) {
645 $this->element('statusnet:'.substr($element, 10), null, $value);
647 $this->element($element, null, $value);
651 $this->elementEnd($tag);
654 function showTwitterXmlGroup($twitter_group)
656 $this->elementStart('group');
657 foreach($twitter_group as $element => $value) {
658 $this->element($element, null, $value);
660 $this->elementEnd('group');
663 function showTwitterXmlList($twitter_list)
665 $this->elementStart('list');
666 foreach($twitter_list as $element => $value) {
667 if($element == 'user') {
668 $this->showTwitterXmlUser($value, 'user');
671 $this->element($element, null, $value);
674 $this->elementEnd('list');
677 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
681 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
683 $this->elementStart($role, $attrs);
684 foreach($twitter_user as $element => $value) {
685 if ($element == 'status') {
686 $this->showTwitterXmlStatus($twitter_user['status']);
687 } else if (strncmp($element, 'statusnet_', 10) == 0) {
688 $this->element('statusnet:'.substr($element, 10), null, $value);
690 $this->element($element, null, $value);
693 $this->elementEnd($role);
696 function showXmlAttachments($attachments) {
697 if (!empty($attachments)) {
698 $this->elementStart('attachments', array('type' => 'array'));
699 foreach ($attachments as $attachment) {
701 $attrs['url'] = $attachment['url'];
702 $attrs['mimetype'] = $attachment['mimetype'];
703 $attrs['size'] = $attachment['size'];
704 $this->element('enclosure', $attrs, '');
706 $this->elementEnd('attachments');
710 function showGeoXML($geo)
714 $this->element('geo');
716 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
717 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
718 $this->elementEnd('geo');
722 function showGeoRSS($geo)
728 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
733 function showTwitterRssItem($entry)
735 $this->elementStart('item');
736 $this->element('title', null, $entry['title']);
737 $this->element('description', null, $entry['description']);
738 $this->element('pubDate', null, $entry['pubDate']);
739 $this->element('guid', null, $entry['guid']);
740 $this->element('link', null, $entry['link']);
742 // RSS only supports 1 enclosure per item
743 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
744 $enclosure = $entry['enclosures'][0];
745 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
748 if(array_key_exists('tags', $entry)){
749 foreach($entry['tags'] as $tag){
750 $this->element('category', null,$tag);
754 $this->showGeoRSS($entry['geo']);
755 $this->elementEnd('item');
758 function showJsonObjects($objects)
760 print(json_encode($objects));
763 function showSingleXmlStatus($notice)
765 $this->initDocument('xml');
766 $twitter_status = $this->twitterStatusArray($notice);
767 $this->showTwitterXmlStatus($twitter_status, 'status', true);
768 $this->endDocument('xml');
771 function showSingleAtomStatus($notice)
773 header('Content-Type: application/atom+xml; charset=utf-8');
774 print $notice->asAtomEntry(true, true, true, $this->auth_user);
777 function show_single_json_status($notice)
779 $this->initDocument('json');
780 $status = $this->twitterStatusArray($notice);
781 $this->showJsonObjects($status);
782 $this->endDocument('json');
785 function showXmlTimeline($notice)
787 $this->initDocument('xml');
788 $this->elementStart('statuses', array('type' => 'array',
789 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
791 if (is_array($notice)) {
792 $notice = new ArrayWrapper($notice);
795 while ($notice->fetch()) {
797 $twitter_status = $this->twitterStatusArray($notice);
798 $this->showTwitterXmlStatus($twitter_status);
799 } catch (Exception $e) {
800 common_log(LOG_ERR, $e->getMessage());
805 $this->elementEnd('statuses');
806 $this->endDocument('xml');
809 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
811 $this->initDocument('rss');
813 $this->element('title', null, $title);
814 $this->element('link', null, $link);
816 if (!is_null($self)) {
820 'type' => 'application/rss+xml',
827 if (!is_null($suplink)) {
828 // For FriendFeed's SUP protocol
829 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
830 'rel' => 'http://api.friendfeed.com/2008/03#sup',
832 'type' => 'application/json'));
835 if (!is_null($logo)) {
836 $this->elementStart('image');
837 $this->element('link', null, $link);
838 $this->element('title', null, $title);
839 $this->element('url', null, $logo);
840 $this->elementEnd('image');
843 $this->element('description', null, $subtitle);
844 $this->element('language', null, 'en-us');
845 $this->element('ttl', null, '40');
847 if (is_array($notice)) {
848 $notice = new ArrayWrapper($notice);
851 while ($notice->fetch()) {
853 $entry = $this->twitterRssEntryArray($notice);
854 $this->showTwitterRssItem($entry);
855 } catch (Exception $e) {
856 common_log(LOG_ERR, $e->getMessage());
857 // continue on exceptions
861 $this->endTwitterRss();
864 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
866 $this->initDocument('atom');
868 $this->element('title', null, $title);
869 $this->element('id', null, $id);
870 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
872 if (!is_null($logo)) {
873 $this->element('logo',null,$logo);
876 if (!is_null($suplink)) {
877 // For FriendFeed's SUP protocol
878 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
880 'type' => 'application/json'));
883 if (!is_null($selfuri)) {
884 $this->element('link', array('href' => $selfuri,
885 'rel' => 'self', 'type' => 'application/atom+xml'), null);
888 $this->element('updated', null, common_date_iso8601('now'));
889 $this->element('subtitle', null, $subtitle);
891 if (is_array($notice)) {
892 $notice = new ArrayWrapper($notice);
895 while ($notice->fetch()) {
897 $this->raw($notice->asAtomEntry());
898 } catch (Exception $e) {
899 common_log(LOG_ERR, $e->getMessage());
904 $this->endDocument('atom');
907 function showRssGroups($group, $title, $link, $subtitle)
909 $this->initDocument('rss');
911 $this->element('title', null, $title);
912 $this->element('link', null, $link);
913 $this->element('description', null, $subtitle);
914 $this->element('language', null, 'en-us');
915 $this->element('ttl', null, '40');
917 if (is_array($group)) {
918 foreach ($group as $g) {
919 $twitter_group = $this->twitterRssGroupArray($g);
920 $this->showTwitterRssItem($twitter_group);
923 while ($group->fetch()) {
924 $twitter_group = $this->twitterRssGroupArray($group);
925 $this->showTwitterRssItem($twitter_group);
929 $this->endTwitterRss();
932 function showTwitterAtomEntry($entry)
934 $this->elementStart('entry');
935 $this->element('title', null, common_xml_safe_str($entry['title']));
938 array('type' => 'html'),
939 common_xml_safe_str($entry['content'])
941 $this->element('id', null, $entry['id']);
942 $this->element('published', null, $entry['published']);
943 $this->element('updated', null, $entry['updated']);
944 $this->element('link', array('type' => 'text/html',
945 'href' => $entry['link'],
946 'rel' => 'alternate'));
947 $this->element('link', array('type' => $entry['avatar-type'],
948 'href' => $entry['avatar'],
950 $this->elementStart('author');
952 $this->element('name', null, $entry['author-name']);
953 $this->element('uri', null, $entry['author-uri']);
955 $this->elementEnd('author');
956 $this->elementEnd('entry');
959 function showXmlDirectMessage($dm, $namespaces=false)
963 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
965 $this->elementStart('direct_message', $attrs);
966 foreach($dm as $element => $value) {
970 $this->showTwitterXmlUser($value, $element);
973 $this->element($element, null, common_xml_safe_str($value));
976 $this->element($element, null, $value);
980 $this->elementEnd('direct_message');
983 function directMessageArray($message)
987 $from_profile = $message->getFrom();
988 $to_profile = $message->getTo();
990 $dmsg['id'] = intval($message->id);
991 $dmsg['sender_id'] = intval($from_profile->id);
992 $dmsg['text'] = trim($message->content);
993 $dmsg['recipient_id'] = intval($to_profile->id);
994 $dmsg['created_at'] = $this->dateTwitter($message->created);
995 $dmsg['sender_screen_name'] = $from_profile->nickname;
996 $dmsg['recipient_screen_name'] = $to_profile->nickname;
997 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
998 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1003 function rssDirectMessageArray($message)
1007 $from = $message->getFrom();
1009 $entry['title'] = sprintf('Message from %1$s to %2$s',
1010 $from->nickname, $message->getTo()->nickname);
1012 $entry['content'] = common_xml_safe_str($message->rendered);
1013 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1014 $entry['published'] = common_date_iso8601($message->created);
1016 $taguribase = TagURI::base();
1018 $entry['id'] = "tag:$taguribase:$entry[link]";
1019 $entry['updated'] = $entry['published'];
1021 $entry['author-name'] = $from->getBestName();
1022 $entry['author-uri'] = $from->homepage;
1024 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1026 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1027 $entry['avatar-type'] = $avatar->mediatype;
1028 } catch (Exception $e) {
1029 $entry['avatar-type'] = 'image/png';
1032 // RSS item specific
1034 $entry['description'] = $entry['content'];
1035 $entry['pubDate'] = common_date_rfc2822($message->created);
1036 $entry['guid'] = $entry['link'];
1041 function showSingleXmlDirectMessage($message)
1043 $this->initDocument('xml');
1044 $dmsg = $this->directMessageArray($message);
1045 $this->showXmlDirectMessage($dmsg, true);
1046 $this->endDocument('xml');
1049 function showSingleJsonDirectMessage($message)
1051 $this->initDocument('json');
1052 $dmsg = $this->directMessageArray($message);
1053 $this->showJsonObjects($dmsg);
1054 $this->endDocument('json');
1057 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1059 $this->initDocument('atom');
1061 $this->element('title', null, common_xml_safe_str($title));
1062 $this->element('id', null, $id);
1063 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1065 if (!is_null($selfuri)) {
1066 $this->element('link', array('href' => $selfuri,
1067 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1070 $this->element('updated', null, common_date_iso8601('now'));
1071 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1073 if (is_array($group)) {
1074 foreach ($group as $g) {
1075 $this->raw($g->asAtomEntry());
1078 while ($group->fetch()) {
1079 $this->raw($group->asAtomEntry());
1083 $this->endDocument('atom');
1087 function showJsonTimeline($notice)
1089 $this->initDocument('json');
1091 $statuses = array();
1093 if (is_array($notice)) {
1094 $notice = new ArrayWrapper($notice);
1097 while ($notice->fetch()) {
1099 $twitter_status = $this->twitterStatusArray($notice);
1100 array_push($statuses, $twitter_status);
1101 } catch (Exception $e) {
1102 common_log(LOG_ERR, $e->getMessage());
1107 $this->showJsonObjects($statuses);
1109 $this->endDocument('json');
1112 function showJsonGroups($group)
1114 $this->initDocument('json');
1118 if (is_array($group)) {
1119 foreach ($group as $g) {
1120 $twitter_group = $this->twitterGroupArray($g);
1121 array_push($groups, $twitter_group);
1124 while ($group->fetch()) {
1125 $twitter_group = $this->twitterGroupArray($group);
1126 array_push($groups, $twitter_group);
1130 $this->showJsonObjects($groups);
1132 $this->endDocument('json');
1135 function showXmlGroups($group)
1138 $this->initDocument('xml');
1139 $this->elementStart('groups', array('type' => 'array'));
1141 if (is_array($group)) {
1142 foreach ($group as $g) {
1143 $twitter_group = $this->twitterGroupArray($g);
1144 $this->showTwitterXmlGroup($twitter_group);
1147 while ($group->fetch()) {
1148 $twitter_group = $this->twitterGroupArray($group);
1149 $this->showTwitterXmlGroup($twitter_group);
1153 $this->elementEnd('groups');
1154 $this->endDocument('xml');
1157 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1160 $this->initDocument('xml');
1161 $this->elementStart('lists_list');
1162 $this->elementStart('lists', array('type' => 'array'));
1164 if (is_array($list)) {
1165 foreach ($list as $l) {
1166 $twitter_list = $this->twitterListArray($l);
1167 $this->showTwitterXmlList($twitter_list);
1170 while ($list->fetch()) {
1171 $twitter_list = $this->twitterListArray($list);
1172 $this->showTwitterXmlList($twitter_list);
1176 $this->elementEnd('lists');
1178 $this->element('next_cursor', null, $next_cursor);
1179 $this->element('previous_cursor', null, $prev_cursor);
1181 $this->elementEnd('lists_list');
1182 $this->endDocument('xml');
1185 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1187 $this->initDocument('json');
1191 if (is_array($list)) {
1192 foreach ($list as $l) {
1193 $twitter_list = $this->twitterListArray($l);
1194 array_push($lists, $twitter_list);
1197 while ($list->fetch()) {
1198 $twitter_list = $this->twitterListArray($list);
1199 array_push($lists, $twitter_list);
1203 $lists_list = array(
1205 'next_cursor' => $next_cursor,
1206 'next_cursor_str' => strval($next_cursor),
1207 'previous_cursor' => $prev_cursor,
1208 'previous_cursor_str' => strval($prev_cursor)
1211 $this->showJsonObjects($lists_list);
1213 $this->endDocument('json');
1216 function showTwitterXmlUsers($user)
1218 $this->initDocument('xml');
1219 $this->elementStart('users', array('type' => 'array',
1220 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1222 if (is_array($user)) {
1223 foreach ($user as $u) {
1224 $twitter_user = $this->twitterUserArray($u);
1225 $this->showTwitterXmlUser($twitter_user);
1228 while ($user->fetch()) {
1229 $twitter_user = $this->twitterUserArray($user);
1230 $this->showTwitterXmlUser($twitter_user);
1234 $this->elementEnd('users');
1235 $this->endDocument('xml');
1238 function showJsonUsers($user)
1240 $this->initDocument('json');
1244 if (is_array($user)) {
1245 foreach ($user as $u) {
1246 $twitter_user = $this->twitterUserArray($u);
1247 array_push($users, $twitter_user);
1250 while ($user->fetch()) {
1251 $twitter_user = $this->twitterUserArray($user);
1252 array_push($users, $twitter_user);
1256 $this->showJsonObjects($users);
1258 $this->endDocument('json');
1261 function showSingleJsonGroup($group)
1263 $this->initDocument('json');
1264 $twitter_group = $this->twitterGroupArray($group);
1265 $this->showJsonObjects($twitter_group);
1266 $this->endDocument('json');
1269 function showSingleXmlGroup($group)
1271 $this->initDocument('xml');
1272 $twitter_group = $this->twitterGroupArray($group);
1273 $this->showTwitterXmlGroup($twitter_group);
1274 $this->endDocument('xml');
1277 function showSingleJsonList($list)
1279 $this->initDocument('json');
1280 $twitter_list = $this->twitterListArray($list);
1281 $this->showJsonObjects($twitter_list);
1282 $this->endDocument('json');
1285 function showSingleXmlList($list)
1287 $this->initDocument('xml');
1288 $twitter_list = $this->twitterListArray($list);
1289 $this->showTwitterXmlList($twitter_list);
1290 $this->endDocument('xml');
1293 function dateTwitter($dt)
1295 $dateStr = date('d F Y H:i:s', strtotime($dt));
1296 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1297 $d->setTimezone(new DateTimeZone(common_timezone()));
1298 return $d->format('D M d H:i:s O Y');
1301 function initDocument($type='xml')
1305 header('Content-Type: application/xml; charset=utf-8');
1309 header('Content-Type: application/json; charset=utf-8');
1311 // Check for JSONP callback
1312 if (isset($this->callback)) {
1313 print $this->callback . '(';
1317 header("Content-Type: application/rss+xml; charset=utf-8");
1318 $this->initTwitterRss();
1321 header('Content-Type: application/atom+xml; charset=utf-8');
1322 $this->initTwitterAtom();
1325 // TRANS: Client error on an API request with an unsupported data format.
1326 $this->clientError(_('Not a supported data format.'));
1332 function endDocument($type='xml')
1339 // Check for JSONP callback
1340 if (isset($this->callback)) {
1345 $this->endTwitterRss();
1348 $this->endTwitterRss();
1351 // TRANS: Client error on an API request with an unsupported data format.
1352 $this->clientError(_('Not a supported data format.'));
1357 function initTwitterRss()
1360 $this->elementStart(
1364 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1365 'xmlns:georss' => 'http://www.georss.org/georss'
1368 $this->elementStart('channel');
1369 Event::handle('StartApiRss', array($this));
1372 function endTwitterRss()
1374 $this->elementEnd('channel');
1375 $this->elementEnd('rss');
1379 function initTwitterAtom()
1382 // FIXME: don't hardcode the language here!
1383 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1384 'xml:lang' => 'en-US',
1385 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1388 function endTwitterAtom()
1390 $this->elementEnd('feed');
1394 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1396 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1397 switch ($content_type) {
1399 $this->showTwitterXmlUser($profile_array);
1402 $this->showJsonObjects($profile_array);
1405 // TRANS: Client error on an API request with an unsupported data format.
1406 $this->clientError(_('Not a supported data format.'));
1411 private static function is_decimal($str)
1413 return preg_match('/^[0-9]+$/', $str);
1416 function getTargetUser($id)
1419 // Twitter supports these other ways of passing the user ID
1420 if (self::is_decimal($this->arg('id'))) {
1421 return User::getKV($this->arg('id'));
1422 } else if ($this->arg('id')) {
1423 $nickname = common_canonical_nickname($this->arg('id'));
1424 return User::getKV('nickname', $nickname);
1425 } else if ($this->arg('user_id')) {
1426 // This is to ensure that a non-numeric user_id still
1427 // overrides screen_name even if it doesn't get used
1428 if (self::is_decimal($this->arg('user_id'))) {
1429 return User::getKV('id', $this->arg('user_id'));
1431 } else if ($this->arg('screen_name')) {
1432 $nickname = common_canonical_nickname($this->arg('screen_name'));
1433 return User::getKV('nickname', $nickname);
1435 // Fall back to trying the currently authenticated user
1436 return $this->auth_user;
1439 } else if (self::is_decimal($id)) {
1440 return User::getKV($id);
1442 $nickname = common_canonical_nickname($id);
1443 return User::getKV('nickname', $nickname);
1447 function getTargetProfile($id)
1451 // Twitter supports these other ways of passing the user ID
1452 if (self::is_decimal($this->arg('id'))) {
1453 return Profile::getKV($this->arg('id'));
1454 } else if ($this->arg('id')) {
1455 // Screen names currently can only uniquely identify a local user.
1456 $nickname = common_canonical_nickname($this->arg('id'));
1457 $user = User::getKV('nickname', $nickname);
1458 return $user ? $user->getProfile() : null;
1459 } else if ($this->arg('user_id')) {
1460 // This is to ensure that a non-numeric user_id still
1461 // overrides screen_name even if it doesn't get used
1462 if (self::is_decimal($this->arg('user_id'))) {
1463 return Profile::getKV('id', $this->arg('user_id'));
1465 } else if ($this->arg('screen_name')) {
1466 $nickname = common_canonical_nickname($this->arg('screen_name'));
1467 $user = User::getKV('nickname', $nickname);
1468 return $user instanceof User ? $user->getProfile() : null;
1470 // Fall back to trying the currently authenticated user
1471 return $this->scoped;
1473 } else if (self::is_decimal($id)) {
1474 return Profile::getKV($id);
1476 $nickname = common_canonical_nickname($id);
1477 $user = User::getKV('nickname', $nickname);
1478 return $user ? $user->getProfile() : null;
1482 function getTargetGroup($id)
1485 if (self::is_decimal($this->arg('id'))) {
1486 return User_group::getKV('id', $this->arg('id'));
1487 } else if ($this->arg('id')) {
1488 return User_group::getForNickname($this->arg('id'));
1489 } else if ($this->arg('group_id')) {
1490 // This is to ensure that a non-numeric group_id still
1491 // overrides group_name even if it doesn't get used
1492 if (self::is_decimal($this->arg('group_id'))) {
1493 return User_group::getKV('id', $this->arg('group_id'));
1495 } else if ($this->arg('group_name')) {
1496 return User_group::getForNickname($this->arg('group_name'));
1499 } else if (self::is_decimal($id)) {
1500 return User_group::getKV('id', $id);
1501 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1502 return User_group::getKV('uri', urldecode($this->arg('uri')));
1504 return User_group::getForNickname($id);
1508 function getTargetList($user=null, $id=null)
1510 $tagger = $this->getTargetUser($user);
1514 $id = $this->arg('id');
1518 if (is_numeric($id)) {
1519 $list = Profile_list::getKV('id', $id);
1521 // only if the list with the id belongs to the tagger
1522 if(empty($list) || $list->tagger != $tagger->id) {
1527 $tag = common_canonical_tag($id);
1528 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1531 if (!empty($list) && $list->private) {
1532 if ($this->auth_user->id == $list->tagger) {
1543 * Returns query argument or default value if not found. Certain
1544 * parameters used throughout the API are lightly scrubbed and
1545 * bounds checked. This overrides Action::arg().
1547 * @param string $key requested argument
1548 * @param string $def default value to return if $key is not provided
1552 function arg($key, $def=null)
1554 // XXX: Do even more input validation/scrubbing?
1556 if (array_key_exists($key, $this->args)) {
1559 $page = (int)$this->args['page'];
1560 return ($page < 1) ? 1 : $page;
1562 $count = (int)$this->args['count'];
1565 } elseif ($count > 200) {
1571 $since_id = (int)$this->args['since_id'];
1572 return ($since_id < 1) ? 0 : $since_id;
1574 $max_id = (int)$this->args['max_id'];
1575 return ($max_id < 1) ? 0 : $max_id;
1577 return parent::arg($key, $def);
1585 * Calculate the complete URI that called up this action. Used for
1586 * Atom rel="self" links. Warning: this is funky.
1588 * @return string URL a URL suitable for rel="self" Atom links
1590 function getSelfUri()
1592 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1594 $id = $this->arg('id');
1595 $aargs = array('format' => $this->format);
1600 $tag = $this->arg('tag');
1602 $aargs['tag'] = $tag;
1605 parse_str($_SERVER['QUERY_STRING'], $params);
1607 if (!empty($params)) {
1608 unset($params['p']);
1609 $pstring = http_build_query($params);
1612 $uri = common_local_url($action, $aargs);
1614 if (!empty($pstring)) {
1615 $uri .= '?' . $pstring;