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 $twitter_user['id'] = intval($profile->id);
206 $twitter_user['name'] = $profile->getBestName();
207 $twitter_user['screen_name'] = $profile->nickname;
208 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
209 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
211 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
212 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
213 Avatar::defaultImage(AVATAR_STREAM_SIZE);
215 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
216 $twitter_user['protected'] = false; # not supported by StatusNet yet
217 $twitter_user['followers_count'] = $profile->subscriberCount();
220 $user = $profile->getUser();
222 // Note: some profiles don't have an associated user
224 $defaultDesign = Design::siteDesign();
227 $design = $user->getDesign();
230 if (empty($design)) {
231 $design = $defaultDesign;
234 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
235 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
236 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
237 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
238 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
239 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
240 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
241 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
242 $twitter_user['profile_sidebar_border_color'] = '';
244 $twitter_user['friends_count'] = $profile->subscriptionCount();
246 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
248 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
252 if (!empty($user) && $user->timezone) {
253 $timezone = $user->timezone;
257 $t->setTimezone(new DateTimeZone($timezone));
259 $twitter_user['utc_offset'] = $t->format('Z');
260 $twitter_user['time_zone'] = $timezone;
262 $twitter_user['profile_background_image_url']
263 = empty($design->backgroundimage)
264 ? '' : ($design->disposition & BACKGROUND_ON)
265 ? Design::url($design->backgroundimage) : '';
267 $twitter_user['profile_background_tile']
268 = (bool)($design->disposition & BACKGROUND_TILE);
270 $twitter_user['statuses_count'] = $profile->noticeCount();
272 // Is the requesting user following this user?
273 $twitter_user['following'] = false;
274 $twitter_user['statusnet:blocking'] = false;
275 $twitter_user['notifications'] = false;
277 if (isset($this->auth_user)) {
279 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
280 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
283 $sub = Subscription::pkeyGet(array('subscriber' =>
284 $this->auth_user->id,
285 'subscribed' => $profile->id));
288 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
293 $notice = $profile->getCurrentNotice();
296 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
300 // StatusNet-specific
302 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
304 return $twitter_user;
307 function twitterStatusArray($notice, $include_user=true)
309 $base = $this->twitterSimpleStatusArray($notice, $include_user);
311 if (!empty($notice->repeat_of)) {
312 $original = Notice::staticGet('id', $notice->repeat_of);
313 if (!empty($original)) {
314 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
315 $base['retweeted_status'] = $original_array;
322 function twitterSimpleStatusArray($notice, $include_user=true)
324 $profile = $notice->getProfile();
326 $twitter_status = array();
327 $twitter_status['text'] = $notice->content;
328 $twitter_status['truncated'] = false; # Not possible on StatusNet
329 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
330 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
331 intval($notice->reply_to) : null;
335 $ns = $notice->getSource();
337 if (!empty($ns->name) && !empty($ns->url)) {
338 $source = '<a href="'
339 . htmlspecialchars($ns->url)
340 . '" rel="nofollow">'
341 . htmlspecialchars($ns->name)
348 $twitter_status['source'] = $source;
349 $twitter_status['id'] = intval($notice->id);
351 $replier_profile = null;
353 if ($notice->reply_to) {
354 $reply = Notice::staticGet(intval($notice->reply_to));
356 $replier_profile = $reply->getProfile();
360 $twitter_status['in_reply_to_user_id'] =
361 ($replier_profile) ? intval($replier_profile->id) : null;
362 $twitter_status['in_reply_to_screen_name'] =
363 ($replier_profile) ? $replier_profile->nickname : null;
365 if (isset($notice->lat) && isset($notice->lon)) {
366 // This is the format that GeoJSON expects stuff to be in
367 $twitter_status['geo'] = array('type' => 'Point',
368 'coordinates' => array((float) $notice->lat,
369 (float) $notice->lon));
371 $twitter_status['geo'] = null;
374 if (isset($this->auth_user)) {
375 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
377 $twitter_status['favorited'] = false;
381 $attachments = $notice->attachments();
383 if (!empty($attachments)) {
385 $twitter_status['attachments'] = array();
387 foreach ($attachments as $attachment) {
388 $enclosure_o=$attachment->getEnclosure();
390 $enclosure = array();
391 $enclosure['url'] = $enclosure_o->url;
392 $enclosure['mimetype'] = $enclosure_o->mimetype;
393 $enclosure['size'] = $enclosure_o->size;
394 $twitter_status['attachments'][] = $enclosure;
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;
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['member_count'] = $group->getMemberCount();
430 $twitter_group['original_logo'] = $group->original_logo;
431 $twitter_group['homepage_logo'] = $group->homepage_logo;
432 $twitter_group['stream_logo'] = $group->stream_logo;
433 $twitter_group['mini_logo'] = $group->mini_logo;
434 $twitter_group['homepage'] = $group->homepage;
435 $twitter_group['description'] = $group->description;
436 $twitter_group['location'] = $group->location;
437 $twitter_group['created'] = $this->dateTwitter($group->created);
438 $twitter_group['modified'] = $this->dateTwitter($group->modified);
440 return $twitter_group;
443 function twitterRssGroupArray($group)
446 $entry['content']=$group->description;
447 $entry['title']=$group->nickname;
448 $entry['link']=$group->permalink();
449 $entry['published']=common_date_iso8601($group->created);
450 $entry['updated']==common_date_iso8601($group->modified);
451 $taguribase = common_config('integration', 'groupuri');
452 $entry['id'] = "group:$groupuribase:$entry[link]";
454 $entry['description'] = $entry['content'];
455 $entry['pubDate'] = common_date_rfc2822($group->created);
456 $entry['guid'] = $entry['link'];
461 function twitterRssEntryArray($notice)
465 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
466 $profile = $notice->getProfile();
468 // We trim() to avoid extraneous whitespace in the output
470 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
471 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
472 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
473 $entry['published'] = common_date_iso8601($notice->created);
475 $taguribase = TagURI::base();
476 $entry['id'] = "tag:$taguribase:$entry[link]";
478 $entry['updated'] = $entry['published'];
479 $entry['author'] = $profile->getBestName();
482 $attachments = $notice->attachments();
483 $enclosures = array();
485 foreach ($attachments as $attachment) {
486 $enclosure_o=$attachment->getEnclosure();
488 $enclosure = array();
489 $enclosure['url'] = $enclosure_o->url;
490 $enclosure['mimetype'] = $enclosure_o->mimetype;
491 $enclosure['size'] = $enclosure_o->size;
492 $enclosures[] = $enclosure;
496 if (!empty($enclosures)) {
497 $entry['enclosures'] = $enclosures;
501 $tag = new Notice_tag();
502 $tag->notice_id = $notice->id;
504 $entry['tags']=array();
505 while ($tag->fetch()) {
506 $entry['tags'][]=$tag->tag;
512 $entry['description'] = $entry['content'];
513 $entry['pubDate'] = common_date_rfc2822($notice->created);
514 $entry['guid'] = $entry['link'];
516 if (isset($notice->lat) && isset($notice->lon)) {
517 // This is the format that GeoJSON expects stuff to be in.
518 // showGeoRSS() below uses it for XML output, so we reuse it
519 $entry['geo'] = array('type' => 'Point',
520 'coordinates' => array((float) $notice->lat,
521 (float) $notice->lon));
523 $entry['geo'] = null;
526 Event::handle('EndRssEntryArray', array($notice, &$entry));
532 function twitterRelationshipArray($source, $target)
534 $relationship = array();
536 $relationship['source'] =
537 $this->relationshipDetailsArray($source, $target);
538 $relationship['target'] =
539 $this->relationshipDetailsArray($target, $source);
541 return array('relationship' => $relationship);
544 function relationshipDetailsArray($source, $target)
548 $details['screen_name'] = $source->nickname;
549 $details['followed_by'] = $target->isSubscribed($source);
550 $details['following'] = $source->isSubscribed($target);
552 $notifications = false;
554 if ($source->isSubscribed($target)) {
555 $sub = Subscription::pkeyGet(array('subscriber' =>
556 $source->id, 'subscribed' => $target->id));
559 $notifications = ($sub->jabber || $sub->sms);
563 $details['notifications_enabled'] = $notifications;
564 $details['blocking'] = $source->hasBlocked($target);
565 $details['id'] = intval($source->id);
570 function showTwitterXmlRelationship($relationship)
572 $this->elementStart('relationship');
574 foreach($relationship as $element => $value) {
575 if ($element == 'source' || $element == 'target') {
576 $this->elementStart($element);
577 $this->showXmlRelationshipDetails($value);
578 $this->elementEnd($element);
582 $this->elementEnd('relationship');
585 function showXmlRelationshipDetails($details)
587 foreach($details as $element => $value) {
588 $this->element($element, null, $value);
592 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
596 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
598 $this->elementStart($tag, $attrs);
599 foreach($twitter_status as $element => $value) {
602 $this->showTwitterXmlUser($twitter_status['user']);
605 $this->element($element, null, common_xml_safe_str($value));
608 $this->showXmlAttachments($twitter_status['attachments']);
611 $this->showGeoXML($value);
613 case 'retweeted_status':
614 $this->showTwitterXmlStatus($value, 'retweeted_status');
617 if (strncmp($element, 'statusnet_', 10) == 0) {
618 $this->element('statusnet:'.substr($element, 10), null, $value);
620 $this->element($element, null, $value);
624 $this->elementEnd($tag);
627 function showTwitterXmlGroup($twitter_group)
629 $this->elementStart('group');
630 foreach($twitter_group as $element => $value) {
631 $this->element($element, null, $value);
633 $this->elementEnd('group');
636 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
640 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
642 $this->elementStart($role, $attrs);
643 foreach($twitter_user as $element => $value) {
644 if ($element == 'status') {
645 $this->showTwitterXmlStatus($twitter_user['status']);
646 } else if (strncmp($element, 'statusnet_', 10) == 0) {
647 $this->element('statusnet:'.substr($element, 10), null, $value);
649 $this->element($element, null, $value);
652 $this->elementEnd($role);
655 function showXmlAttachments($attachments) {
656 if (!empty($attachments)) {
657 $this->elementStart('attachments', array('type' => 'array'));
658 foreach ($attachments as $attachment) {
660 $attrs['url'] = $attachment['url'];
661 $attrs['mimetype'] = $attachment['mimetype'];
662 $attrs['size'] = $attachment['size'];
663 $this->element('enclosure', $attrs, '');
665 $this->elementEnd('attachments');
669 function showGeoXML($geo)
673 $this->element('geo');
675 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
676 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
677 $this->elementEnd('geo');
681 function showGeoRSS($geo)
687 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
692 function showTwitterRssItem($entry)
694 $this->elementStart('item');
695 $this->element('title', null, $entry['title']);
696 $this->element('description', null, $entry['description']);
697 $this->element('pubDate', null, $entry['pubDate']);
698 $this->element('guid', null, $entry['guid']);
699 $this->element('link', null, $entry['link']);
701 // RSS only supports 1 enclosure per item
702 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
703 $enclosure = $entry['enclosures'][0];
704 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
707 if(array_key_exists('tags', $entry)){
708 foreach($entry['tags'] as $tag){
709 $this->element('category', null,$tag);
713 $this->showGeoRSS($entry['geo']);
714 $this->elementEnd('item');
717 function showJsonObjects($objects)
719 print(json_encode($objects));
722 function showSingleXmlStatus($notice)
724 $this->initDocument('xml');
725 $twitter_status = $this->twitterStatusArray($notice);
726 $this->showTwitterXmlStatus($twitter_status, 'status', true);
727 $this->endDocument('xml');
730 function showSingleAtomStatus($notice)
732 header('Content-Type: application/atom+xml; charset=utf-8');
733 print $notice->asAtomEntry(true, true, true, $this->auth_user);
736 function show_single_json_status($notice)
738 $this->initDocument('json');
739 $status = $this->twitterStatusArray($notice);
740 $this->showJsonObjects($status);
741 $this->endDocument('json');
744 function showXmlTimeline($notice)
746 $this->initDocument('xml');
747 $this->elementStart('statuses', array('type' => 'array',
748 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
750 if (is_array($notice)) {
751 $notice = new ArrayWrapper($notice);
754 while ($notice->fetch()) {
756 $twitter_status = $this->twitterStatusArray($notice);
757 $this->showTwitterXmlStatus($twitter_status);
758 } catch (Exception $e) {
759 common_log(LOG_ERR, $e->getMessage());
764 $this->elementEnd('statuses');
765 $this->endDocument('xml');
768 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
770 $this->initDocument('rss');
772 $this->element('title', null, $title);
773 $this->element('link', null, $link);
775 if (!is_null($self)) {
779 'type' => 'application/rss+xml',
786 if (!is_null($suplink)) {
787 // For FriendFeed's SUP protocol
788 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
789 'rel' => 'http://api.friendfeed.com/2008/03#sup',
791 'type' => 'application/json'));
794 if (!is_null($logo)) {
795 $this->elementStart('image');
796 $this->element('link', null, $link);
797 $this->element('title', null, $title);
798 $this->element('url', null, $logo);
799 $this->elementEnd('image');
802 $this->element('description', null, $subtitle);
803 $this->element('language', null, 'en-us');
804 $this->element('ttl', null, '40');
806 if (is_array($notice)) {
807 $notice = new ArrayWrapper($notice);
810 while ($notice->fetch()) {
812 $entry = $this->twitterRssEntryArray($notice);
813 $this->showTwitterRssItem($entry);
814 } catch (Exception $e) {
815 common_log(LOG_ERR, $e->getMessage());
816 // continue on exceptions
820 $this->endTwitterRss();
823 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
825 $this->initDocument('atom');
827 $this->element('title', null, $title);
828 $this->element('id', null, $id);
829 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
831 if (!is_null($logo)) {
832 $this->element('logo',null,$logo);
835 if (!is_null($suplink)) {
836 // For FriendFeed's SUP protocol
837 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
839 'type' => 'application/json'));
842 if (!is_null($selfuri)) {
843 $this->element('link', array('href' => $selfuri,
844 'rel' => 'self', 'type' => 'application/atom+xml'), null);
847 $this->element('updated', null, common_date_iso8601('now'));
848 $this->element('subtitle', null, $subtitle);
850 if (is_array($notice)) {
851 $notice = new ArrayWrapper($notice);
854 while ($notice->fetch()) {
856 $this->raw($notice->asAtomEntry());
857 } catch (Exception $e) {
858 common_log(LOG_ERR, $e->getMessage());
863 $this->endDocument('atom');
866 function showRssGroups($group, $title, $link, $subtitle)
868 $this->initDocument('rss');
870 $this->element('title', null, $title);
871 $this->element('link', null, $link);
872 $this->element('description', null, $subtitle);
873 $this->element('language', null, 'en-us');
874 $this->element('ttl', null, '40');
876 if (is_array($group)) {
877 foreach ($group as $g) {
878 $twitter_group = $this->twitterRssGroupArray($g);
879 $this->showTwitterRssItem($twitter_group);
882 while ($group->fetch()) {
883 $twitter_group = $this->twitterRssGroupArray($group);
884 $this->showTwitterRssItem($twitter_group);
888 $this->endTwitterRss();
891 function showTwitterAtomEntry($entry)
893 $this->elementStart('entry');
894 $this->element('title', null, common_xml_safe_str($entry['title']));
897 array('type' => 'html'),
898 common_xml_safe_str($entry['content'])
900 $this->element('id', null, $entry['id']);
901 $this->element('published', null, $entry['published']);
902 $this->element('updated', null, $entry['updated']);
903 $this->element('link', array('type' => 'text/html',
904 'href' => $entry['link'],
905 'rel' => 'alternate'));
906 $this->element('link', array('type' => $entry['avatar-type'],
907 'href' => $entry['avatar'],
909 $this->elementStart('author');
911 $this->element('name', null, $entry['author-name']);
912 $this->element('uri', null, $entry['author-uri']);
914 $this->elementEnd('author');
915 $this->elementEnd('entry');
918 function showXmlDirectMessage($dm, $namespaces=false)
922 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
924 $this->elementStart('direct_message', $attrs);
925 foreach($dm as $element => $value) {
929 $this->showTwitterXmlUser($value, $element);
932 $this->element($element, null, common_xml_safe_str($value));
935 $this->element($element, null, $value);
939 $this->elementEnd('direct_message');
942 function directMessageArray($message)
946 $from_profile = $message->getFrom();
947 $to_profile = $message->getTo();
949 $dmsg['id'] = intval($message->id);
950 $dmsg['sender_id'] = intval($from_profile);
951 $dmsg['text'] = trim($message->content);
952 $dmsg['recipient_id'] = intval($to_profile);
953 $dmsg['created_at'] = $this->dateTwitter($message->created);
954 $dmsg['sender_screen_name'] = $from_profile->nickname;
955 $dmsg['recipient_screen_name'] = $to_profile->nickname;
956 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
957 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
962 function rssDirectMessageArray($message)
966 $from = $message->getFrom();
968 $entry['title'] = sprintf('Message from %1$s to %2$s',
969 $from->nickname, $message->getTo()->nickname);
971 $entry['content'] = common_xml_safe_str($message->rendered);
972 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
973 $entry['published'] = common_date_iso8601($message->created);
975 $taguribase = TagURI::base();
977 $entry['id'] = "tag:$taguribase:$entry[link]";
978 $entry['updated'] = $entry['published'];
980 $entry['author-name'] = $from->getBestName();
981 $entry['author-uri'] = $from->homepage;
983 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
985 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
986 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
990 $entry['description'] = $entry['content'];
991 $entry['pubDate'] = common_date_rfc2822($message->created);
992 $entry['guid'] = $entry['link'];
997 function showSingleXmlDirectMessage($message)
999 $this->initDocument('xml');
1000 $dmsg = $this->directMessageArray($message);
1001 $this->showXmlDirectMessage($dmsg, true);
1002 $this->endDocument('xml');
1005 function showSingleJsonDirectMessage($message)
1007 $this->initDocument('json');
1008 $dmsg = $this->directMessageArray($message);
1009 $this->showJsonObjects($dmsg);
1010 $this->endDocument('json');
1013 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1015 $this->initDocument('atom');
1017 $this->element('title', null, common_xml_safe_str($title));
1018 $this->element('id', null, $id);
1019 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1021 if (!is_null($selfuri)) {
1022 $this->element('link', array('href' => $selfuri,
1023 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1026 $this->element('updated', null, common_date_iso8601('now'));
1027 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1029 if (is_array($group)) {
1030 foreach ($group as $g) {
1031 $this->raw($g->asAtomEntry());
1034 while ($group->fetch()) {
1035 $this->raw($group->asAtomEntry());
1039 $this->endDocument('atom');
1043 function showJsonTimeline($notice)
1045 $this->initDocument('json');
1047 $statuses = array();
1049 if (is_array($notice)) {
1050 $notice = new ArrayWrapper($notice);
1053 while ($notice->fetch()) {
1055 $twitter_status = $this->twitterStatusArray($notice);
1056 array_push($statuses, $twitter_status);
1057 } catch (Exception $e) {
1058 common_log(LOG_ERR, $e->getMessage());
1063 $this->showJsonObjects($statuses);
1065 $this->endDocument('json');
1068 function showJsonGroups($group)
1070 $this->initDocument('json');
1074 if (is_array($group)) {
1075 foreach ($group as $g) {
1076 $twitter_group = $this->twitterGroupArray($g);
1077 array_push($groups, $twitter_group);
1080 while ($group->fetch()) {
1081 $twitter_group = $this->twitterGroupArray($group);
1082 array_push($groups, $twitter_group);
1086 $this->showJsonObjects($groups);
1088 $this->endDocument('json');
1091 function showXmlGroups($group)
1094 $this->initDocument('xml');
1095 $this->elementStart('groups', array('type' => 'array'));
1097 if (is_array($group)) {
1098 foreach ($group as $g) {
1099 $twitter_group = $this->twitterGroupArray($g);
1100 $this->showTwitterXmlGroup($twitter_group);
1103 while ($group->fetch()) {
1104 $twitter_group = $this->twitterGroupArray($group);
1105 $this->showTwitterXmlGroup($twitter_group);
1109 $this->elementEnd('groups');
1110 $this->endDocument('xml');
1113 function showTwitterXmlUsers($user)
1115 $this->initDocument('xml');
1116 $this->elementStart('users', array('type' => 'array',
1117 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1119 if (is_array($user)) {
1120 foreach ($user as $u) {
1121 $twitter_user = $this->twitterUserArray($u);
1122 $this->showTwitterXmlUser($twitter_user);
1125 while ($user->fetch()) {
1126 $twitter_user = $this->twitterUserArray($user);
1127 $this->showTwitterXmlUser($twitter_user);
1131 $this->elementEnd('users');
1132 $this->endDocument('xml');
1135 function showJsonUsers($user)
1137 $this->initDocument('json');
1141 if (is_array($user)) {
1142 foreach ($user as $u) {
1143 $twitter_user = $this->twitterUserArray($u);
1144 array_push($users, $twitter_user);
1147 while ($user->fetch()) {
1148 $twitter_user = $this->twitterUserArray($user);
1149 array_push($users, $twitter_user);
1153 $this->showJsonObjects($users);
1155 $this->endDocument('json');
1158 function showSingleJsonGroup($group)
1160 $this->initDocument('json');
1161 $twitter_group = $this->twitterGroupArray($group);
1162 $this->showJsonObjects($twitter_group);
1163 $this->endDocument('json');
1166 function showSingleXmlGroup($group)
1168 $this->initDocument('xml');
1169 $twitter_group = $this->twitterGroupArray($group);
1170 $this->showTwitterXmlGroup($twitter_group);
1171 $this->endDocument('xml');
1174 function dateTwitter($dt)
1176 $dateStr = date('d F Y H:i:s', strtotime($dt));
1177 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1178 $d->setTimezone(new DateTimeZone(common_timezone()));
1179 return $d->format('D M d H:i:s O Y');
1182 function initDocument($type='xml')
1186 header('Content-Type: application/xml; charset=utf-8');
1190 header('Content-Type: application/json; charset=utf-8');
1192 // Check for JSONP callback
1193 if (isset($this->callback)) {
1194 print $this->callback . '(';
1198 header("Content-Type: application/rss+xml; charset=utf-8");
1199 $this->initTwitterRss();
1202 header('Content-Type: application/atom+xml; charset=utf-8');
1203 $this->initTwitterAtom();
1206 // TRANS: Client error on an API request with an unsupported data format.
1207 $this->clientError(_('Not a supported data format.'));
1214 function endDocument($type='xml')
1221 // Check for JSONP callback
1222 if (isset($this->callback)) {
1227 $this->endTwitterRss();
1230 $this->endTwitterRss();
1233 // TRANS: Client error on an API request with an unsupported data format.
1234 $this->clientError(_('Not a supported data format.'));
1240 function clientError($msg, $code = 400, $format = null)
1242 $action = $this->trimmed('action');
1243 if ($format === null) {
1244 $format = $this->format;
1247 common_debug("User error '$code' on '$action': $msg", __FILE__);
1249 if (!array_key_exists($code, ClientErrorAction::$status)) {
1253 $status_string = ClientErrorAction::$status[$code];
1255 // Do not emit error header for JSONP
1256 if (!isset($this->callback)) {
1257 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1262 $this->initDocument('xml');
1263 $this->elementStart('hash');
1264 $this->element('error', null, $msg);
1265 $this->element('request', null, $_SERVER['REQUEST_URI']);
1266 $this->elementEnd('hash');
1267 $this->endDocument('xml');
1270 $this->initDocument('json');
1271 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1272 print(json_encode($error_array));
1273 $this->endDocument('json');
1276 header('Content-Type: text/plain; charset=utf-8');
1280 // If user didn't request a useful format, throw a regular client error
1281 throw new ClientException($msg, $code);
1285 function serverError($msg, $code = 500, $content_type = null)
1287 $action = $this->trimmed('action');
1288 if ($content_type === null) {
1289 $content_type = $this->format;
1292 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1294 if (!array_key_exists($code, ServerErrorAction::$status)) {
1298 $status_string = ServerErrorAction::$status[$code];
1300 // Do not emit error header for JSONP
1301 if (!isset($this->callback)) {
1302 header('HTTP/1.1 '.$code.' '.$status_string);
1305 if ($content_type == 'xml') {
1306 $this->initDocument('xml');
1307 $this->elementStart('hash');
1308 $this->element('error', null, $msg);
1309 $this->element('request', null, $_SERVER['REQUEST_URI']);
1310 $this->elementEnd('hash');
1311 $this->endDocument('xml');
1313 $this->initDocument('json');
1314 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1315 print(json_encode($error_array));
1316 $this->endDocument('json');
1320 function initTwitterRss()
1323 $this->elementStart(
1327 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1328 'xmlns:georss' => 'http://www.georss.org/georss'
1331 $this->elementStart('channel');
1332 Event::handle('StartApiRss', array($this));
1335 function endTwitterRss()
1337 $this->elementEnd('channel');
1338 $this->elementEnd('rss');
1342 function initTwitterAtom()
1345 // FIXME: don't hardcode the language here!
1346 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1347 'xml:lang' => 'en-US',
1348 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1351 function endTwitterAtom()
1353 $this->elementEnd('feed');
1357 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1359 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1360 switch ($content_type) {
1362 $this->showTwitterXmlUser($profile_array);
1365 $this->showJsonObjects($profile_array);
1368 // TRANS: Client error on an API request with an unsupported data format.
1369 $this->clientError(_('Not a supported data format.'));
1375 private static function is_decimal($str)
1377 return preg_match('/^[0-9]+$/', $str);
1380 function getTargetUser($id)
1383 // Twitter supports these other ways of passing the user ID
1384 if (self::is_decimal($this->arg('id'))) {
1385 return User::staticGet($this->arg('id'));
1386 } else if ($this->arg('id')) {
1387 $nickname = common_canonical_nickname($this->arg('id'));
1388 return User::staticGet('nickname', $nickname);
1389 } else if ($this->arg('user_id')) {
1390 // This is to ensure that a non-numeric user_id still
1391 // overrides screen_name even if it doesn't get used
1392 if (self::is_decimal($this->arg('user_id'))) {
1393 return User::staticGet('id', $this->arg('user_id'));
1395 } else if ($this->arg('screen_name')) {
1396 $nickname = common_canonical_nickname($this->arg('screen_name'));
1397 return User::staticGet('nickname', $nickname);
1399 // Fall back to trying the currently authenticated user
1400 return $this->auth_user;
1403 } else if (self::is_decimal($id)) {
1404 return User::staticGet($id);
1406 $nickname = common_canonical_nickname($id);
1407 return User::staticGet('nickname', $nickname);
1411 function getTargetProfile($id)
1415 // Twitter supports these other ways of passing the user ID
1416 if (self::is_decimal($this->arg('id'))) {
1417 return Profile::staticGet($this->arg('id'));
1418 } else if ($this->arg('id')) {
1419 // Screen names currently can only uniquely identify a local user.
1420 $nickname = common_canonical_nickname($this->arg('id'));
1421 $user = User::staticGet('nickname', $nickname);
1422 return $user ? $user->getProfile() : null;
1423 } else if ($this->arg('user_id')) {
1424 // This is to ensure that a non-numeric user_id still
1425 // overrides screen_name even if it doesn't get used
1426 if (self::is_decimal($this->arg('user_id'))) {
1427 return Profile::staticGet('id', $this->arg('user_id'));
1429 } else if ($this->arg('screen_name')) {
1430 $nickname = common_canonical_nickname($this->arg('screen_name'));
1431 $user = User::staticGet('nickname', $nickname);
1432 return $user ? $user->getProfile() : null;
1434 } else if (self::is_decimal($id)) {
1435 return Profile::staticGet($id);
1437 $nickname = common_canonical_nickname($id);
1438 $user = User::staticGet('nickname', $nickname);
1439 return $user ? $user->getProfile() : null;
1443 function getTargetGroup($id)
1446 if (self::is_decimal($this->arg('id'))) {
1447 return User_group::staticGet('id', $this->arg('id'));
1448 } else if ($this->arg('id')) {
1449 return User_group::getForNickname($this->arg('id'));
1450 } else if ($this->arg('group_id')) {
1451 // This is to ensure that a non-numeric group_id still
1452 // overrides group_name even if it doesn't get used
1453 if (self::is_decimal($this->arg('group_id'))) {
1454 return User_group::staticGet('id', $this->arg('group_id'));
1456 } else if ($this->arg('group_name')) {
1457 return User_group::getForNickname($this->arg('group_name'));
1460 } else if (self::is_decimal($id)) {
1461 return User_group::staticGet('id', $id);
1463 return User_group::getForNickname($id);
1468 * Returns query argument or default value if not found. Certain
1469 * parameters used throughout the API are lightly scrubbed and
1470 * bounds checked. This overrides Action::arg().
1472 * @param string $key requested argument
1473 * @param string $def default value to return if $key is not provided
1477 function arg($key, $def=null)
1479 // XXX: Do even more input validation/scrubbing?
1481 if (array_key_exists($key, $this->args)) {
1484 $page = (int)$this->args['page'];
1485 return ($page < 1) ? 1 : $page;
1487 $count = (int)$this->args['count'];
1490 } elseif ($count > 200) {
1496 $since_id = (int)$this->args['since_id'];
1497 return ($since_id < 1) ? 0 : $since_id;
1499 $max_id = (int)$this->args['max_id'];
1500 return ($max_id < 1) ? 0 : $max_id;
1502 return parent::arg($key, $def);
1510 * Calculate the complete URI that called up this action. Used for
1511 * Atom rel="self" links. Warning: this is funky.
1513 * @return string URL a URL suitable for rel="self" Atom links
1515 function getSelfUri()
1517 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1519 $id = $this->arg('id');
1520 $aargs = array('format' => $this->format);
1525 $tag = $this->arg('tag');
1527 $aargs['tag'] = $tag;
1530 parse_str($_SERVER['QUERY_STRING'], $params);
1532 if (!empty($params)) {
1533 unset($params['p']);
1534 $pstring = http_build_query($params);
1537 $uri = common_local_url($action, $aargs);
1539 if (!empty($pstring)) {
1540 $uri .= '?' . $pstring;