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;
124 var $auth_user = null;
128 var $since_id = null;
130 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 function prepare($args)
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 function handle($args)
177 header('Access-Control-Allow-Origin: *');
178 parent::handle($args);
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();
205 $user = $profile->getUser();
207 $twitter_user['id'] = intval($profile->id);
208 $twitter_user['name'] = $profile->getBestName();
209 $twitter_user['screen_name'] = $profile->nickname;
210 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
211 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
213 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
214 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
215 Avatar::defaultImage(AVATAR_STREAM_SIZE);
217 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
218 $twitter_user['protected'] = ($user->private_stream) ? true : false;
219 $twitter_user['followers_count'] = $profile->subscriberCount();
221 // Note: some profiles don't have an associated user
223 $twitter_user['friends_count'] = $profile->subscriptionCount();
225 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
227 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
231 if (!empty($user) && $user->timezone) {
232 $timezone = $user->timezone;
236 $t->setTimezone(new DateTimeZone($timezone));
238 $twitter_user['utc_offset'] = $t->format('Z');
239 $twitter_user['time_zone'] = $timezone;
240 $twitter_user['statuses_count'] = $profile->noticeCount();
242 // Is the requesting user following this user?
243 $twitter_user['following'] = false;
244 $twitter_user['statusnet_blocking'] = false;
245 $twitter_user['notifications'] = false;
247 if (isset($this->auth_user)) {
249 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
250 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
253 $sub = Subscription::pkeyGet(array('subscriber' =>
254 $this->auth_user->id,
255 'subscribed' => $profile->id));
258 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
263 $notice = $profile->getCurrentNotice();
266 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
270 // StatusNet-specific
272 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
274 return $twitter_user;
277 function twitterStatusArray($notice, $include_user=true)
279 $base = $this->twitterSimpleStatusArray($notice, $include_user);
281 if (!empty($notice->repeat_of)) {
282 $original = Notice::staticGet('id', $notice->repeat_of);
283 if (!empty($original)) {
284 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
285 $base['retweeted_status'] = $original_array;
292 function twitterSimpleStatusArray($notice, $include_user=true)
294 $profile = $notice->getProfile();
296 $twitter_status = array();
297 $twitter_status['text'] = $notice->content;
298 $twitter_status['truncated'] = false; # Not possible on StatusNet
299 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
300 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
301 intval($notice->reply_to) : null;
305 $ns = $notice->getSource();
307 if (!empty($ns->name) && !empty($ns->url)) {
308 $source = '<a href="'
309 . htmlspecialchars($ns->url)
310 . '" rel="nofollow">'
311 . htmlspecialchars($ns->name)
318 $twitter_status['source'] = $source;
319 $twitter_status['id'] = intval($notice->id);
321 $replier_profile = null;
323 if ($notice->reply_to) {
324 $reply = Notice::staticGet(intval($notice->reply_to));
326 $replier_profile = $reply->getProfile();
330 $twitter_status['in_reply_to_user_id'] =
331 ($replier_profile) ? intval($replier_profile->id) : null;
332 $twitter_status['in_reply_to_screen_name'] =
333 ($replier_profile) ? $replier_profile->nickname : null;
335 if (isset($notice->lat) && isset($notice->lon)) {
336 // This is the format that GeoJSON expects stuff to be in
337 $twitter_status['geo'] = array('type' => 'Point',
338 'coordinates' => array((float) $notice->lat,
339 (float) $notice->lon));
341 $twitter_status['geo'] = null;
344 if (isset($this->auth_user)) {
345 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
347 $twitter_status['favorited'] = false;
351 $attachments = $notice->attachments();
353 if (!empty($attachments)) {
355 $twitter_status['attachments'] = array();
357 foreach ($attachments as $attachment) {
358 $enclosure_o=$attachment->getEnclosure();
360 $enclosure = array();
361 $enclosure['url'] = $enclosure_o->url;
362 $enclosure['mimetype'] = $enclosure_o->mimetype;
363 $enclosure['size'] = $enclosure_o->size;
364 $twitter_status['attachments'][] = $enclosure;
369 if ($include_user && $profile) {
370 // Don't get notice (recursive!)
371 $twitter_user = $this->twitterUserArray($profile, false);
372 $twitter_status['user'] = $twitter_user;
375 // StatusNet-specific
377 $twitter_status['statusnet_html'] = $notice->rendered;
378 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
380 return $twitter_status;
383 function twitterGroupArray($group)
385 $twitter_group = array();
387 $twitter_group['id'] = intval($group->id);
388 $twitter_group['url'] = $group->permalink();
389 $twitter_group['nickname'] = $group->nickname;
390 $twitter_group['fullname'] = $group->fullname;
392 if (isset($this->auth_user)) {
393 $twitter_group['member'] = $this->auth_user->isMember($group);
394 $twitter_group['blocked'] = Group_block::isBlocked(
396 $this->auth_user->getProfile()
400 $twitter_group['member_count'] = $group->getMemberCount();
401 $twitter_group['original_logo'] = $group->original_logo;
402 $twitter_group['homepage_logo'] = $group->homepage_logo;
403 $twitter_group['stream_logo'] = $group->stream_logo;
404 $twitter_group['mini_logo'] = $group->mini_logo;
405 $twitter_group['homepage'] = $group->homepage;
406 $twitter_group['description'] = $group->description;
407 $twitter_group['location'] = $group->location;
408 $twitter_group['created'] = $this->dateTwitter($group->created);
409 $twitter_group['modified'] = $this->dateTwitter($group->modified);
411 return $twitter_group;
414 function twitterRssGroupArray($group)
417 $entry['content']=$group->description;
418 $entry['title']=$group->nickname;
419 $entry['link']=$group->permalink();
420 $entry['published']=common_date_iso8601($group->created);
421 $entry['updated']==common_date_iso8601($group->modified);
422 $taguribase = common_config('integration', 'groupuri');
423 $entry['id'] = "group:$groupuribase:$entry[link]";
425 $entry['description'] = $entry['content'];
426 $entry['pubDate'] = common_date_rfc2822($group->created);
427 $entry['guid'] = $entry['link'];
432 function twitterListArray($list)
434 $profile = Profile::staticGet('id', $list->tagger);
436 $twitter_list = array();
437 $twitter_list['id'] = $list->id;
438 $twitter_list['name'] = $list->tag;
439 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
440 $twitter_list['slug'] = $list->tag;
441 $twitter_list['description'] = $list->description;
442 $twitter_list['subscriber_count'] = $list->subscriberCount();
443 $twitter_list['member_count'] = $list->taggedCount();
444 $twitter_list['uri'] = $list->getUri();
446 if (isset($this->auth_user)) {
447 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
449 $twitter_list['following'] = false;
452 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
453 $twitter_list['user'] = $this->twitterUserArray($profile, false);
455 return $twitter_list;
458 function twitterRssEntryArray($notice)
462 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
463 $profile = $notice->getProfile();
465 // We trim() to avoid extraneous whitespace in the output
467 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
468 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
469 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
470 $entry['published'] = common_date_iso8601($notice->created);
472 $taguribase = TagURI::base();
473 $entry['id'] = "tag:$taguribase:$entry[link]";
475 $entry['updated'] = $entry['published'];
476 $entry['author'] = $profile->getBestName();
479 $attachments = $notice->attachments();
480 $enclosures = array();
482 foreach ($attachments as $attachment) {
483 $enclosure_o=$attachment->getEnclosure();
485 $enclosure = array();
486 $enclosure['url'] = $enclosure_o->url;
487 $enclosure['mimetype'] = $enclosure_o->mimetype;
488 $enclosure['size'] = $enclosure_o->size;
489 $enclosures[] = $enclosure;
493 if (!empty($enclosures)) {
494 $entry['enclosures'] = $enclosures;
498 $tag = new Notice_tag();
499 $tag->notice_id = $notice->id;
501 $entry['tags']=array();
502 while ($tag->fetch()) {
503 $entry['tags'][]=$tag->tag;
509 $entry['description'] = $entry['content'];
510 $entry['pubDate'] = common_date_rfc2822($notice->created);
511 $entry['guid'] = $entry['link'];
513 if (isset($notice->lat) && isset($notice->lon)) {
514 // This is the format that GeoJSON expects stuff to be in.
515 // showGeoRSS() below uses it for XML output, so we reuse it
516 $entry['geo'] = array('type' => 'Point',
517 'coordinates' => array((float) $notice->lat,
518 (float) $notice->lon));
520 $entry['geo'] = null;
523 Event::handle('EndRssEntryArray', array($notice, &$entry));
529 function twitterRelationshipArray($source, $target)
531 $relationship = array();
533 $relationship['source'] =
534 $this->relationshipDetailsArray($source, $target);
535 $relationship['target'] =
536 $this->relationshipDetailsArray($target, $source);
538 return array('relationship' => $relationship);
541 function relationshipDetailsArray($source, $target)
545 $details['screen_name'] = $source->nickname;
546 $details['followed_by'] = $target->isSubscribed($source);
547 $details['following'] = $source->isSubscribed($target);
549 $notifications = false;
551 if ($source->isSubscribed($target)) {
552 $sub = Subscription::pkeyGet(array('subscriber' =>
553 $source->id, 'subscribed' => $target->id));
556 $notifications = ($sub->jabber || $sub->sms);
560 $details['notifications_enabled'] = $notifications;
561 $details['blocking'] = $source->hasBlocked($target);
562 $details['id'] = intval($source->id);
567 function showTwitterXmlRelationship($relationship)
569 $this->elementStart('relationship');
571 foreach($relationship as $element => $value) {
572 if ($element == 'source' || $element == 'target') {
573 $this->elementStart($element);
574 $this->showXmlRelationshipDetails($value);
575 $this->elementEnd($element);
579 $this->elementEnd('relationship');
582 function showXmlRelationshipDetails($details)
584 foreach($details as $element => $value) {
585 $this->element($element, null, $value);
589 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
593 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
595 $this->elementStart($tag, $attrs);
596 foreach($twitter_status as $element => $value) {
599 $this->showTwitterXmlUser($twitter_status['user']);
602 $this->element($element, null, common_xml_safe_str($value));
605 $this->showXmlAttachments($twitter_status['attachments']);
608 $this->showGeoXML($value);
610 case 'retweeted_status':
611 $this->showTwitterXmlStatus($value, 'retweeted_status');
614 if (strncmp($element, 'statusnet_', 10) == 0) {
615 $this->element('statusnet:'.substr($element, 10), null, $value);
617 $this->element($element, null, $value);
621 $this->elementEnd($tag);
624 function showTwitterXmlGroup($twitter_group)
626 $this->elementStart('group');
627 foreach($twitter_group as $element => $value) {
628 $this->element($element, null, $value);
630 $this->elementEnd('group');
633 function showTwitterXmlList($twitter_list)
635 $this->elementStart('list');
636 foreach($twitter_list as $element => $value) {
637 if($element == 'user') {
638 $this->showTwitterXmlUser($value, 'user');
641 $this->element($element, null, $value);
644 $this->elementEnd('list');
647 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
651 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
653 $this->elementStart($role, $attrs);
654 foreach($twitter_user as $element => $value) {
655 if ($element == 'status') {
656 $this->showTwitterXmlStatus($twitter_user['status']);
657 } else if (strncmp($element, 'statusnet_', 10) == 0) {
658 $this->element('statusnet:'.substr($element, 10), null, $value);
660 $this->element($element, null, $value);
663 $this->elementEnd($role);
666 function showXmlAttachments($attachments) {
667 if (!empty($attachments)) {
668 $this->elementStart('attachments', array('type' => 'array'));
669 foreach ($attachments as $attachment) {
671 $attrs['url'] = $attachment['url'];
672 $attrs['mimetype'] = $attachment['mimetype'];
673 $attrs['size'] = $attachment['size'];
674 $this->element('enclosure', $attrs, '');
676 $this->elementEnd('attachments');
680 function showGeoXML($geo)
684 $this->element('geo');
686 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
687 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
688 $this->elementEnd('geo');
692 function showGeoRSS($geo)
698 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
703 function showTwitterRssItem($entry)
705 $this->elementStart('item');
706 $this->element('title', null, $entry['title']);
707 $this->element('description', null, $entry['description']);
708 $this->element('pubDate', null, $entry['pubDate']);
709 $this->element('guid', null, $entry['guid']);
710 $this->element('link', null, $entry['link']);
712 // RSS only supports 1 enclosure per item
713 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
714 $enclosure = $entry['enclosures'][0];
715 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
718 if(array_key_exists('tags', $entry)){
719 foreach($entry['tags'] as $tag){
720 $this->element('category', null,$tag);
724 $this->showGeoRSS($entry['geo']);
725 $this->elementEnd('item');
728 function showJsonObjects($objects)
730 print(json_encode($objects));
733 function showSingleXmlStatus($notice)
735 $this->initDocument('xml');
736 $twitter_status = $this->twitterStatusArray($notice);
737 $this->showTwitterXmlStatus($twitter_status, 'status', true);
738 $this->endDocument('xml');
741 function showSingleAtomStatus($notice)
743 header('Content-Type: application/atom+xml; charset=utf-8');
744 print $notice->asAtomEntry(true, true, true, $this->auth_user);
747 function show_single_json_status($notice)
749 $this->initDocument('json');
750 $status = $this->twitterStatusArray($notice);
751 $this->showJsonObjects($status);
752 $this->endDocument('json');
755 function showXmlTimeline($notice)
757 $this->initDocument('xml');
758 $this->elementStart('statuses', array('type' => 'array',
759 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
761 if (is_array($notice)) {
762 $notice = new ArrayWrapper($notice);
765 while ($notice->fetch()) {
767 $twitter_status = $this->twitterStatusArray($notice);
768 $this->showTwitterXmlStatus($twitter_status);
769 } catch (Exception $e) {
770 common_log(LOG_ERR, $e->getMessage());
775 $this->elementEnd('statuses');
776 $this->endDocument('xml');
779 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
781 $this->initDocument('rss');
783 $this->element('title', null, $title);
784 $this->element('link', null, $link);
786 if (!is_null($self)) {
790 'type' => 'application/rss+xml',
797 if (!is_null($suplink)) {
798 // For FriendFeed's SUP protocol
799 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
800 'rel' => 'http://api.friendfeed.com/2008/03#sup',
802 'type' => 'application/json'));
805 if (!is_null($logo)) {
806 $this->elementStart('image');
807 $this->element('link', null, $link);
808 $this->element('title', null, $title);
809 $this->element('url', null, $logo);
810 $this->elementEnd('image');
813 $this->element('description', null, $subtitle);
814 $this->element('language', null, 'en-us');
815 $this->element('ttl', null, '40');
817 if (is_array($notice)) {
818 $notice = new ArrayWrapper($notice);
821 while ($notice->fetch()) {
823 $entry = $this->twitterRssEntryArray($notice);
824 $this->showTwitterRssItem($entry);
825 } catch (Exception $e) {
826 common_log(LOG_ERR, $e->getMessage());
827 // continue on exceptions
831 $this->endTwitterRss();
834 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
836 $this->initDocument('atom');
838 $this->element('title', null, $title);
839 $this->element('id', null, $id);
840 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
842 if (!is_null($logo)) {
843 $this->element('logo',null,$logo);
846 if (!is_null($suplink)) {
847 // For FriendFeed's SUP protocol
848 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
850 'type' => 'application/json'));
853 if (!is_null($selfuri)) {
854 $this->element('link', array('href' => $selfuri,
855 'rel' => 'self', 'type' => 'application/atom+xml'), null);
858 $this->element('updated', null, common_date_iso8601('now'));
859 $this->element('subtitle', null, $subtitle);
861 if (is_array($notice)) {
862 $notice = new ArrayWrapper($notice);
865 while ($notice->fetch()) {
867 $this->raw($notice->asAtomEntry());
868 } catch (Exception $e) {
869 common_log(LOG_ERR, $e->getMessage());
874 $this->endDocument('atom');
877 function showRssGroups($group, $title, $link, $subtitle)
879 $this->initDocument('rss');
881 $this->element('title', null, $title);
882 $this->element('link', null, $link);
883 $this->element('description', null, $subtitle);
884 $this->element('language', null, 'en-us');
885 $this->element('ttl', null, '40');
887 if (is_array($group)) {
888 foreach ($group as $g) {
889 $twitter_group = $this->twitterRssGroupArray($g);
890 $this->showTwitterRssItem($twitter_group);
893 while ($group->fetch()) {
894 $twitter_group = $this->twitterRssGroupArray($group);
895 $this->showTwitterRssItem($twitter_group);
899 $this->endTwitterRss();
902 function showTwitterAtomEntry($entry)
904 $this->elementStart('entry');
905 $this->element('title', null, common_xml_safe_str($entry['title']));
908 array('type' => 'html'),
909 common_xml_safe_str($entry['content'])
911 $this->element('id', null, $entry['id']);
912 $this->element('published', null, $entry['published']);
913 $this->element('updated', null, $entry['updated']);
914 $this->element('link', array('type' => 'text/html',
915 'href' => $entry['link'],
916 'rel' => 'alternate'));
917 $this->element('link', array('type' => $entry['avatar-type'],
918 'href' => $entry['avatar'],
920 $this->elementStart('author');
922 $this->element('name', null, $entry['author-name']);
923 $this->element('uri', null, $entry['author-uri']);
925 $this->elementEnd('author');
926 $this->elementEnd('entry');
929 function showXmlDirectMessage($dm, $namespaces=false)
933 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
935 $this->elementStart('direct_message', $attrs);
936 foreach($dm as $element => $value) {
940 $this->showTwitterXmlUser($value, $element);
943 $this->element($element, null, common_xml_safe_str($value));
946 $this->element($element, null, $value);
950 $this->elementEnd('direct_message');
953 function directMessageArray($message)
957 $from_profile = $message->getFrom();
958 $to_profile = $message->getTo();
960 $dmsg['id'] = intval($message->id);
961 $dmsg['sender_id'] = intval($from_profile);
962 $dmsg['text'] = trim($message->content);
963 $dmsg['recipient_id'] = intval($to_profile);
964 $dmsg['created_at'] = $this->dateTwitter($message->created);
965 $dmsg['sender_screen_name'] = $from_profile->nickname;
966 $dmsg['recipient_screen_name'] = $to_profile->nickname;
967 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
968 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
973 function rssDirectMessageArray($message)
977 $from = $message->getFrom();
979 $entry['title'] = sprintf('Message from %1$s to %2$s',
980 $from->nickname, $message->getTo()->nickname);
982 $entry['content'] = common_xml_safe_str($message->rendered);
983 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
984 $entry['published'] = common_date_iso8601($message->created);
986 $taguribase = TagURI::base();
988 $entry['id'] = "tag:$taguribase:$entry[link]";
989 $entry['updated'] = $entry['published'];
991 $entry['author-name'] = $from->getBestName();
992 $entry['author-uri'] = $from->homepage;
994 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
996 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
997 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
1001 $entry['description'] = $entry['content'];
1002 $entry['pubDate'] = common_date_rfc2822($message->created);
1003 $entry['guid'] = $entry['link'];
1008 function showSingleXmlDirectMessage($message)
1010 $this->initDocument('xml');
1011 $dmsg = $this->directMessageArray($message);
1012 $this->showXmlDirectMessage($dmsg, true);
1013 $this->endDocument('xml');
1016 function showSingleJsonDirectMessage($message)
1018 $this->initDocument('json');
1019 $dmsg = $this->directMessageArray($message);
1020 $this->showJsonObjects($dmsg);
1021 $this->endDocument('json');
1024 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1026 $this->initDocument('atom');
1028 $this->element('title', null, common_xml_safe_str($title));
1029 $this->element('id', null, $id);
1030 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1032 if (!is_null($selfuri)) {
1033 $this->element('link', array('href' => $selfuri,
1034 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1037 $this->element('updated', null, common_date_iso8601('now'));
1038 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1040 if (is_array($group)) {
1041 foreach ($group as $g) {
1042 $this->raw($g->asAtomEntry());
1045 while ($group->fetch()) {
1046 $this->raw($group->asAtomEntry());
1050 $this->endDocument('atom');
1054 function showJsonTimeline($notice)
1056 $this->initDocument('json');
1058 $statuses = array();
1060 if (is_array($notice)) {
1061 $notice = new ArrayWrapper($notice);
1064 while ($notice->fetch()) {
1066 $twitter_status = $this->twitterStatusArray($notice);
1067 array_push($statuses, $twitter_status);
1068 } catch (Exception $e) {
1069 common_log(LOG_ERR, $e->getMessage());
1074 $this->showJsonObjects($statuses);
1076 $this->endDocument('json');
1079 function showJsonGroups($group)
1081 $this->initDocument('json');
1085 if (is_array($group)) {
1086 foreach ($group as $g) {
1087 $twitter_group = $this->twitterGroupArray($g);
1088 array_push($groups, $twitter_group);
1091 while ($group->fetch()) {
1092 $twitter_group = $this->twitterGroupArray($group);
1093 array_push($groups, $twitter_group);
1097 $this->showJsonObjects($groups);
1099 $this->endDocument('json');
1102 function showXmlGroups($group)
1105 $this->initDocument('xml');
1106 $this->elementStart('groups', array('type' => 'array'));
1108 if (is_array($group)) {
1109 foreach ($group as $g) {
1110 $twitter_group = $this->twitterGroupArray($g);
1111 $this->showTwitterXmlGroup($twitter_group);
1114 while ($group->fetch()) {
1115 $twitter_group = $this->twitterGroupArray($group);
1116 $this->showTwitterXmlGroup($twitter_group);
1120 $this->elementEnd('groups');
1121 $this->endDocument('xml');
1124 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1127 $this->initDocument('xml');
1128 $this->elementStart('lists_list');
1129 $this->elementStart('lists', array('type' => 'array'));
1131 if (is_array($list)) {
1132 foreach ($list as $l) {
1133 $twitter_list = $this->twitterListArray($l);
1134 $this->showTwitterXmlList($twitter_list);
1137 while ($list->fetch()) {
1138 $twitter_list = $this->twitterListArray($list);
1139 $this->showTwitterXmlList($twitter_list);
1143 $this->elementEnd('lists');
1145 $this->element('next_cursor', null, $next_cursor);
1146 $this->element('previous_cursor', null, $prev_cursor);
1148 $this->elementEnd('lists_list');
1149 $this->endDocument('xml');
1152 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1154 $this->initDocument('json');
1158 if (is_array($list)) {
1159 foreach ($list as $l) {
1160 $twitter_list = $this->twitterListArray($l);
1161 array_push($lists, $twitter_list);
1164 while ($list->fetch()) {
1165 $twitter_list = $this->twitterListArray($list);
1166 array_push($lists, $twitter_list);
1170 $lists_list = array(
1172 'next_cursor' => $next_cursor,
1173 'next_cursor_str' => strval($next_cursor),
1174 'previous_cursor' => $prev_cursor,
1175 'previous_cursor_str' => strval($prev_cursor)
1178 $this->showJsonObjects($lists_list);
1180 $this->endDocument('json');
1183 function showTwitterXmlUsers($user)
1185 $this->initDocument('xml');
1186 $this->elementStart('users', array('type' => 'array',
1187 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1189 if (is_array($user)) {
1190 foreach ($user as $u) {
1191 $twitter_user = $this->twitterUserArray($u);
1192 $this->showTwitterXmlUser($twitter_user);
1195 while ($user->fetch()) {
1196 $twitter_user = $this->twitterUserArray($user);
1197 $this->showTwitterXmlUser($twitter_user);
1201 $this->elementEnd('users');
1202 $this->endDocument('xml');
1205 function showJsonUsers($user)
1207 $this->initDocument('json');
1211 if (is_array($user)) {
1212 foreach ($user as $u) {
1213 $twitter_user = $this->twitterUserArray($u);
1214 array_push($users, $twitter_user);
1217 while ($user->fetch()) {
1218 $twitter_user = $this->twitterUserArray($user);
1219 array_push($users, $twitter_user);
1223 $this->showJsonObjects($users);
1225 $this->endDocument('json');
1228 function showSingleJsonGroup($group)
1230 $this->initDocument('json');
1231 $twitter_group = $this->twitterGroupArray($group);
1232 $this->showJsonObjects($twitter_group);
1233 $this->endDocument('json');
1236 function showSingleXmlGroup($group)
1238 $this->initDocument('xml');
1239 $twitter_group = $this->twitterGroupArray($group);
1240 $this->showTwitterXmlGroup($twitter_group);
1241 $this->endDocument('xml');
1244 function showSingleJsonList($list)
1246 $this->initDocument('json');
1247 $twitter_list = $this->twitterListArray($list);
1248 $this->showJsonObjects($twitter_list);
1249 $this->endDocument('json');
1252 function showSingleXmlList($list)
1254 $this->initDocument('xml');
1255 $twitter_list = $this->twitterListArray($list);
1256 $this->showTwitterXmlList($twitter_list);
1257 $this->endDocument('xml');
1260 function dateTwitter($dt)
1262 $dateStr = date('d F Y H:i:s', strtotime($dt));
1263 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1264 $d->setTimezone(new DateTimeZone(common_timezone()));
1265 return $d->format('D M d H:i:s O Y');
1268 function initDocument($type='xml')
1272 header('Content-Type: application/xml; charset=utf-8');
1276 header('Content-Type: application/json; charset=utf-8');
1278 // Check for JSONP callback
1279 if (isset($this->callback)) {
1280 print $this->callback . '(';
1284 header("Content-Type: application/rss+xml; charset=utf-8");
1285 $this->initTwitterRss();
1288 header('Content-Type: application/atom+xml; charset=utf-8');
1289 $this->initTwitterAtom();
1292 // TRANS: Client error on an API request with an unsupported data format.
1293 $this->clientError(_('Not a supported data format.'));
1300 function endDocument($type='xml')
1307 // Check for JSONP callback
1308 if (isset($this->callback)) {
1313 $this->endTwitterRss();
1316 $this->endTwitterRss();
1319 // TRANS: Client error on an API request with an unsupported data format.
1320 $this->clientError(_('Not a supported data format.'));
1326 function clientError($msg, $code = 400, $format = null)
1328 $action = $this->trimmed('action');
1329 if ($format === null) {
1330 $format = $this->format;
1333 common_debug("User error '$code' on '$action': $msg", __FILE__);
1335 if (!array_key_exists($code, ClientErrorAction::$status)) {
1339 $status_string = ClientErrorAction::$status[$code];
1341 // Do not emit error header for JSONP
1342 if (!isset($this->callback)) {
1343 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1348 $this->initDocument('xml');
1349 $this->elementStart('hash');
1350 $this->element('error', null, $msg);
1351 $this->element('request', null, $_SERVER['REQUEST_URI']);
1352 $this->elementEnd('hash');
1353 $this->endDocument('xml');
1356 $this->initDocument('json');
1357 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1358 print(json_encode($error_array));
1359 $this->endDocument('json');
1362 header('Content-Type: text/plain; charset=utf-8');
1366 // If user didn't request a useful format, throw a regular client error
1367 throw new ClientException($msg, $code);
1371 function serverError($msg, $code = 500, $content_type = null)
1373 $action = $this->trimmed('action');
1374 if ($content_type === null) {
1375 $content_type = $this->format;
1378 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1380 if (!array_key_exists($code, ServerErrorAction::$status)) {
1384 $status_string = ServerErrorAction::$status[$code];
1386 // Do not emit error header for JSONP
1387 if (!isset($this->callback)) {
1388 header('HTTP/1.1 '.$code.' '.$status_string);
1391 if ($content_type == 'xml') {
1392 $this->initDocument('xml');
1393 $this->elementStart('hash');
1394 $this->element('error', null, $msg);
1395 $this->element('request', null, $_SERVER['REQUEST_URI']);
1396 $this->elementEnd('hash');
1397 $this->endDocument('xml');
1399 $this->initDocument('json');
1400 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1401 print(json_encode($error_array));
1402 $this->endDocument('json');
1406 function initTwitterRss()
1409 $this->elementStart(
1413 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1414 'xmlns:georss' => 'http://www.georss.org/georss'
1417 $this->elementStart('channel');
1418 Event::handle('StartApiRss', array($this));
1421 function endTwitterRss()
1423 $this->elementEnd('channel');
1424 $this->elementEnd('rss');
1428 function initTwitterAtom()
1431 // FIXME: don't hardcode the language here!
1432 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1433 'xml:lang' => 'en-US',
1434 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1437 function endTwitterAtom()
1439 $this->elementEnd('feed');
1443 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1445 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1446 switch ($content_type) {
1448 $this->showTwitterXmlUser($profile_array);
1451 $this->showJsonObjects($profile_array);
1454 // TRANS: Client error on an API request with an unsupported data format.
1455 $this->clientError(_('Not a supported data format.'));
1461 private static function is_decimal($str)
1463 return preg_match('/^[0-9]+$/', $str);
1466 function getTargetUser($id)
1469 // Twitter supports these other ways of passing the user ID
1470 if (self::is_decimal($this->arg('id'))) {
1471 return User::staticGet($this->arg('id'));
1472 } else if ($this->arg('id')) {
1473 $nickname = common_canonical_nickname($this->arg('id'));
1474 return User::staticGet('nickname', $nickname);
1475 } else if ($this->arg('user_id')) {
1476 // This is to ensure that a non-numeric user_id still
1477 // overrides screen_name even if it doesn't get used
1478 if (self::is_decimal($this->arg('user_id'))) {
1479 return User::staticGet('id', $this->arg('user_id'));
1481 } else if ($this->arg('screen_name')) {
1482 $nickname = common_canonical_nickname($this->arg('screen_name'));
1483 return User::staticGet('nickname', $nickname);
1485 // Fall back to trying the currently authenticated user
1486 return $this->auth_user;
1489 } else if (self::is_decimal($id)) {
1490 return User::staticGet($id);
1492 $nickname = common_canonical_nickname($id);
1493 return User::staticGet('nickname', $nickname);
1497 function getTargetProfile($id)
1501 // Twitter supports these other ways of passing the user ID
1502 if (self::is_decimal($this->arg('id'))) {
1503 return Profile::staticGet($this->arg('id'));
1504 } else if ($this->arg('id')) {
1505 // Screen names currently can only uniquely identify a local user.
1506 $nickname = common_canonical_nickname($this->arg('id'));
1507 $user = User::staticGet('nickname', $nickname);
1508 return $user ? $user->getProfile() : null;
1509 } else if ($this->arg('user_id')) {
1510 // This is to ensure that a non-numeric user_id still
1511 // overrides screen_name even if it doesn't get used
1512 if (self::is_decimal($this->arg('user_id'))) {
1513 return Profile::staticGet('id', $this->arg('user_id'));
1515 } else if ($this->arg('screen_name')) {
1516 $nickname = common_canonical_nickname($this->arg('screen_name'));
1517 $user = User::staticGet('nickname', $nickname);
1518 return $user ? $user->getProfile() : null;
1520 } else if (self::is_decimal($id)) {
1521 return Profile::staticGet($id);
1523 $nickname = common_canonical_nickname($id);
1524 $user = User::staticGet('nickname', $nickname);
1525 return $user ? $user->getProfile() : null;
1529 function getTargetGroup($id)
1532 if (self::is_decimal($this->arg('id'))) {
1533 return User_group::staticGet('id', $this->arg('id'));
1534 } else if ($this->arg('id')) {
1535 return User_group::getForNickname($this->arg('id'));
1536 } else if ($this->arg('group_id')) {
1537 // This is to ensure that a non-numeric group_id still
1538 // overrides group_name even if it doesn't get used
1539 if (self::is_decimal($this->arg('group_id'))) {
1540 return User_group::staticGet('id', $this->arg('group_id'));
1542 } else if ($this->arg('group_name')) {
1543 return User_group::getForNickname($this->arg('group_name'));
1546 } else if (self::is_decimal($id)) {
1547 return User_group::staticGet('id', $id);
1549 return User_group::getForNickname($id);
1553 function getTargetList($user=null, $id=null)
1555 $tagger = $this->getTargetUser($user);
1559 $id = $this->arg('id');
1563 if (is_numeric($id)) {
1564 $list = Profile_list::staticGet('id', $id);
1566 // only if the list with the id belongs to the tagger
1567 if(empty($list) || $list->tagger != $tagger->id) {
1572 $tag = common_canonical_tag($id);
1573 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1576 if (!empty($list) && $list->private) {
1577 if ($this->auth_user->id == $list->tagger) {
1588 * Returns query argument or default value if not found. Certain
1589 * parameters used throughout the API are lightly scrubbed and
1590 * bounds checked. This overrides Action::arg().
1592 * @param string $key requested argument
1593 * @param string $def default value to return if $key is not provided
1597 function arg($key, $def=null)
1599 // XXX: Do even more input validation/scrubbing?
1601 if (array_key_exists($key, $this->args)) {
1604 $page = (int)$this->args['page'];
1605 return ($page < 1) ? 1 : $page;
1607 $count = (int)$this->args['count'];
1610 } elseif ($count > 200) {
1616 $since_id = (int)$this->args['since_id'];
1617 return ($since_id < 1) ? 0 : $since_id;
1619 $max_id = (int)$this->args['max_id'];
1620 return ($max_id < 1) ? 0 : $max_id;
1622 return parent::arg($key, $def);
1630 * Calculate the complete URI that called up this action. Used for
1631 * Atom rel="self" links. Warning: this is funky.
1633 * @return string URL a URL suitable for rel="self" Atom links
1635 function getSelfUri()
1637 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1639 $id = $this->arg('id');
1640 $aargs = array('format' => $this->format);
1645 $tag = $this->arg('tag');
1647 $aargs['tag'] = $tag;
1650 parse_str($_SERVER['QUERY_STRING'], $params);
1652 if (!empty($params)) {
1653 unset($params['p']);
1654 $pstring = http_build_query($params);
1657 $uri = common_local_url($action, $aargs);
1659 if (!empty($pstring)) {
1660 $uri .= '?' . $pstring;