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 = empty($design->disposition)
269 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
271 $twitter_user['statuses_count'] = $profile->noticeCount();
273 // Is the requesting user following this user?
274 $twitter_user['following'] = false;
275 $twitter_user['statusnet:blocking'] = false;
276 $twitter_user['notifications'] = false;
278 if (isset($this->auth_user)) {
280 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
281 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
284 $sub = Subscription::pkeyGet(array('subscriber' =>
285 $this->auth_user->id,
286 'subscribed' => $profile->id));
289 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
294 $notice = $profile->getCurrentNotice();
297 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
301 // StatusNet-specific
303 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
305 return $twitter_user;
308 function twitterStatusArray($notice, $include_user=true)
310 $base = $this->twitterSimpleStatusArray($notice, $include_user);
312 if (!empty($notice->repeat_of)) {
313 $original = Notice::staticGet('id', $notice->repeat_of);
314 if (!empty($original)) {
315 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
316 $base['retweeted_status'] = $original_array;
323 function twitterSimpleStatusArray($notice, $include_user=true)
325 $profile = $notice->getProfile();
327 $twitter_status = array();
328 $twitter_status['text'] = $notice->content;
329 $twitter_status['truncated'] = false; # Not possible on StatusNet
330 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
331 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
332 intval($notice->reply_to) : null;
336 $ns = $notice->getSource();
338 if (!empty($ns->name) && !empty($ns->url)) {
339 $source = '<a href="'
340 . htmlspecialchars($ns->url)
341 . '" rel="nofollow">'
342 . htmlspecialchars($ns->name)
349 $twitter_status['source'] = $source;
350 $twitter_status['id'] = intval($notice->id);
352 $replier_profile = null;
354 if ($notice->reply_to) {
355 $reply = Notice::staticGet(intval($notice->reply_to));
357 $replier_profile = $reply->getProfile();
361 $twitter_status['in_reply_to_user_id'] =
362 ($replier_profile) ? intval($replier_profile->id) : null;
363 $twitter_status['in_reply_to_screen_name'] =
364 ($replier_profile) ? $replier_profile->nickname : null;
366 if (isset($notice->lat) && isset($notice->lon)) {
367 // This is the format that GeoJSON expects stuff to be in
368 $twitter_status['geo'] = array('type' => 'Point',
369 'coordinates' => array((float) $notice->lat,
370 (float) $notice->lon));
372 $twitter_status['geo'] = null;
375 if (isset($this->auth_user)) {
376 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
378 $twitter_status['favorited'] = false;
382 $attachments = $notice->attachments();
384 if (!empty($attachments)) {
386 $twitter_status['attachments'] = array();
388 foreach ($attachments as $attachment) {
389 $enclosure_o=$attachment->getEnclosure();
391 $enclosure = array();
392 $enclosure['url'] = $enclosure_o->url;
393 $enclosure['mimetype'] = $enclosure_o->mimetype;
394 $enclosure['size'] = $enclosure_o->size;
395 $twitter_status['attachments'][] = $enclosure;
400 if ($include_user && $profile) {
401 # Don't get notice (recursive!)
402 $twitter_user = $this->twitterUserArray($profile, false);
403 $twitter_status['user'] = $twitter_user;
406 // StatusNet-specific
408 $twitter_status['statusnet_html'] = $notice->rendered;
410 return $twitter_status;
413 function twitterGroupArray($group)
415 $twitter_group = array();
417 $twitter_group['id'] = $group->id;
418 $twitter_group['url'] = $group->permalink();
419 $twitter_group['nickname'] = $group->nickname;
420 $twitter_group['fullname'] = $group->fullname;
422 if (isset($this->auth_user)) {
423 $twitter_group['member'] = $this->auth_user->isMember($group);
424 $twitter_group['blocked'] = Group_block::isBlocked(
426 $this->auth_user->getProfile()
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 twitterRssEntryArray($notice)
466 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
467 $profile = $notice->getProfile();
469 // We trim() to avoid extraneous whitespace in the output
471 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
472 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
473 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
474 $entry['published'] = common_date_iso8601($notice->created);
476 $taguribase = TagURI::base();
477 $entry['id'] = "tag:$taguribase:$entry[link]";
479 $entry['updated'] = $entry['published'];
480 $entry['author'] = $profile->getBestName();
483 $attachments = $notice->attachments();
484 $enclosures = array();
486 foreach ($attachments as $attachment) {
487 $enclosure_o=$attachment->getEnclosure();
489 $enclosure = array();
490 $enclosure['url'] = $enclosure_o->url;
491 $enclosure['mimetype'] = $enclosure_o->mimetype;
492 $enclosure['size'] = $enclosure_o->size;
493 $enclosures[] = $enclosure;
497 if (!empty($enclosures)) {
498 $entry['enclosures'] = $enclosures;
502 $tag = new Notice_tag();
503 $tag->notice_id = $notice->id;
505 $entry['tags']=array();
506 while ($tag->fetch()) {
507 $entry['tags'][]=$tag->tag;
513 $entry['description'] = $entry['content'];
514 $entry['pubDate'] = common_date_rfc2822($notice->created);
515 $entry['guid'] = $entry['link'];
517 if (isset($notice->lat) && isset($notice->lon)) {
518 // This is the format that GeoJSON expects stuff to be in.
519 // showGeoRSS() below uses it for XML output, so we reuse it
520 $entry['geo'] = array('type' => 'Point',
521 'coordinates' => array((float) $notice->lat,
522 (float) $notice->lon));
524 $entry['geo'] = null;
527 Event::handle('EndRssEntryArray', array($notice, &$entry));
533 function twitterRelationshipArray($source, $target)
535 $relationship = array();
537 $relationship['source'] =
538 $this->relationshipDetailsArray($source, $target);
539 $relationship['target'] =
540 $this->relationshipDetailsArray($target, $source);
542 return array('relationship' => $relationship);
545 function relationshipDetailsArray($source, $target)
549 $details['screen_name'] = $source->nickname;
550 $details['followed_by'] = $target->isSubscribed($source);
551 $details['following'] = $source->isSubscribed($target);
553 $notifications = false;
555 if ($source->isSubscribed($target)) {
556 $sub = Subscription::pkeyGet(array('subscriber' =>
557 $source->id, 'subscribed' => $target->id));
560 $notifications = ($sub->jabber || $sub->sms);
564 $details['notifications_enabled'] = $notifications;
565 $details['blocking'] = $source->hasBlocked($target);
566 $details['id'] = $source->id;
571 function showTwitterXmlRelationship($relationship)
573 $this->elementStart('relationship');
575 foreach($relationship as $element => $value) {
576 if ($element == 'source' || $element == 'target') {
577 $this->elementStart($element);
578 $this->showXmlRelationshipDetails($value);
579 $this->elementEnd($element);
583 $this->elementEnd('relationship');
586 function showXmlRelationshipDetails($details)
588 foreach($details as $element => $value) {
589 $this->element($element, null, $value);
593 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
597 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
599 $this->elementStart($tag, $attrs);
600 foreach($twitter_status as $element => $value) {
603 $this->showTwitterXmlUser($twitter_status['user']);
606 $this->element($element, null, common_xml_safe_str($value));
609 $this->showXmlAttachments($twitter_status['attachments']);
612 $this->showGeoXML($value);
614 case 'retweeted_status':
615 $this->showTwitterXmlStatus($value, 'retweeted_status');
618 if (strncmp($element, 'statusnet_', 10) == 0) {
619 $this->element('statusnet:'.substr($element, 10), null, $value);
621 $this->element($element, null, $value);
625 $this->elementEnd($tag);
628 function showTwitterXmlGroup($twitter_group)
630 $this->elementStart('group');
631 foreach($twitter_group as $element => $value) {
632 $this->element($element, null, $value);
634 $this->elementEnd('group');
637 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
641 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
643 $this->elementStart($role, $attrs);
644 foreach($twitter_user as $element => $value) {
645 if ($element == 'status') {
646 $this->showTwitterXmlStatus($twitter_user['status']);
647 } else if (strncmp($element, 'statusnet_', 10) == 0) {
648 $this->element('statusnet:'.substr($element, 10), null, $value);
650 $this->element($element, null, $value);
653 $this->elementEnd($role);
656 function showXmlAttachments($attachments) {
657 if (!empty($attachments)) {
658 $this->elementStart('attachments', array('type' => 'array'));
659 foreach ($attachments as $attachment) {
661 $attrs['url'] = $attachment['url'];
662 $attrs['mimetype'] = $attachment['mimetype'];
663 $attrs['size'] = $attachment['size'];
664 $this->element('enclosure', $attrs, '');
666 $this->elementEnd('attachments');
670 function showGeoXML($geo)
674 $this->element('geo');
676 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
677 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
678 $this->elementEnd('geo');
682 function showGeoRSS($geo)
688 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
693 function showTwitterRssItem($entry)
695 $this->elementStart('item');
696 $this->element('title', null, $entry['title']);
697 $this->element('description', null, $entry['description']);
698 $this->element('pubDate', null, $entry['pubDate']);
699 $this->element('guid', null, $entry['guid']);
700 $this->element('link', null, $entry['link']);
702 # RSS only supports 1 enclosure per item
703 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
704 $enclosure = $entry['enclosures'][0];
705 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
708 if(array_key_exists('tags', $entry)){
709 foreach($entry['tags'] as $tag){
710 $this->element('category', null,$tag);
714 $this->showGeoRSS($entry['geo']);
715 $this->elementEnd('item');
718 function showJsonObjects($objects)
720 print(json_encode($objects));
723 function showSingleXmlStatus($notice)
725 $this->initDocument('xml');
726 $twitter_status = $this->twitterStatusArray($notice);
727 $this->showTwitterXmlStatus($twitter_status, 'status', true);
728 $this->endDocument('xml');
731 function showSingleAtomStatus($notice)
733 header('Content-Type: application/atom+xml; charset=utf-8');
734 print $notice->asAtomEntry(true, true, true, $this->auth_user);
737 function show_single_json_status($notice)
739 $this->initDocument('json');
740 $status = $this->twitterStatusArray($notice);
741 $this->showJsonObjects($status);
742 $this->endDocument('json');
745 function showXmlTimeline($notice)
747 $this->initDocument('xml');
748 $this->elementStart('statuses', array('type' => 'array',
749 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
751 if (is_array($notice)) {
752 $notice = new ArrayWrapper($notice);
755 while ($notice->fetch()) {
757 $twitter_status = $this->twitterStatusArray($notice);
758 $this->showTwitterXmlStatus($twitter_status);
759 } catch (Exception $e) {
760 common_log(LOG_ERR, $e->getMessage());
765 $this->elementEnd('statuses');
766 $this->endDocument('xml');
769 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
771 $this->initDocument('rss');
773 $this->element('title', null, $title);
774 $this->element('link', null, $link);
776 if (!is_null($self)) {
780 'type' => 'application/rss+xml',
787 if (!is_null($suplink)) {
788 // For FriendFeed's SUP protocol
789 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
790 'rel' => 'http://api.friendfeed.com/2008/03#sup',
792 'type' => 'application/json'));
795 if (!is_null($logo)) {
796 $this->elementStart('image');
797 $this->element('link', null, $link);
798 $this->element('title', null, $title);
799 $this->element('url', null, $logo);
800 $this->elementEnd('image');
803 $this->element('description', null, $subtitle);
804 $this->element('language', null, 'en-us');
805 $this->element('ttl', null, '40');
807 if (is_array($notice)) {
808 $notice = new ArrayWrapper($notice);
811 while ($notice->fetch()) {
813 $entry = $this->twitterRssEntryArray($notice);
814 $this->showTwitterRssItem($entry);
815 } catch (Exception $e) {
816 common_log(LOG_ERR, $e->getMessage());
817 // continue on exceptions
821 $this->endTwitterRss();
824 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
826 $this->initDocument('atom');
828 $this->element('title', null, $title);
829 $this->element('id', null, $id);
830 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
832 if (!is_null($logo)) {
833 $this->element('logo',null,$logo);
836 if (!is_null($suplink)) {
837 # For FriendFeed's SUP protocol
838 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
840 'type' => 'application/json'));
843 if (!is_null($selfuri)) {
844 $this->element('link', array('href' => $selfuri,
845 'rel' => 'self', 'type' => 'application/atom+xml'), null);
848 $this->element('updated', null, common_date_iso8601('now'));
849 $this->element('subtitle', null, $subtitle);
851 if (is_array($notice)) {
852 $notice = new ArrayWrapper($notice);
855 while ($notice->fetch()) {
857 $this->raw($notice->asAtomEntry());
858 } catch (Exception $e) {
859 common_log(LOG_ERR, $e->getMessage());
864 $this->endDocument('atom');
867 function showRssGroups($group, $title, $link, $subtitle)
869 $this->initDocument('rss');
871 $this->element('title', null, $title);
872 $this->element('link', null, $link);
873 $this->element('description', null, $subtitle);
874 $this->element('language', null, 'en-us');
875 $this->element('ttl', null, '40');
877 if (is_array($group)) {
878 foreach ($group as $g) {
879 $twitter_group = $this->twitterRssGroupArray($g);
880 $this->showTwitterRssItem($twitter_group);
883 while ($group->fetch()) {
884 $twitter_group = $this->twitterRssGroupArray($group);
885 $this->showTwitterRssItem($twitter_group);
889 $this->endTwitterRss();
892 function showTwitterAtomEntry($entry)
894 $this->elementStart('entry');
895 $this->element('title', null, common_xml_safe_str($entry['title']));
898 array('type' => 'html'),
899 common_xml_safe_str($entry['content'])
901 $this->element('id', null, $entry['id']);
902 $this->element('published', null, $entry['published']);
903 $this->element('updated', null, $entry['updated']);
904 $this->element('link', array('type' => 'text/html',
905 'href' => $entry['link'],
906 'rel' => 'alternate'));
907 $this->element('link', array('type' => $entry['avatar-type'],
908 'href' => $entry['avatar'],
910 $this->elementStart('author');
912 $this->element('name', null, $entry['author-name']);
913 $this->element('uri', null, $entry['author-uri']);
915 $this->elementEnd('author');
916 $this->elementEnd('entry');
919 function showXmlDirectMessage($dm, $namespaces=false)
923 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
925 $this->elementStart('direct_message', $attrs);
926 foreach($dm as $element => $value) {
930 $this->showTwitterXmlUser($value, $element);
933 $this->element($element, null, common_xml_safe_str($value));
936 $this->element($element, null, $value);
940 $this->elementEnd('direct_message');
943 function directMessageArray($message)
947 $from_profile = $message->getFrom();
948 $to_profile = $message->getTo();
950 $dmsg['id'] = $message->id;
951 $dmsg['sender_id'] = $message->from_profile;
952 $dmsg['text'] = trim($message->content);
953 $dmsg['recipient_id'] = $message->to_profile;
954 $dmsg['created_at'] = $this->dateTwitter($message->created);
955 $dmsg['sender_screen_name'] = $from_profile->nickname;
956 $dmsg['recipient_screen_name'] = $to_profile->nickname;
957 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
958 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
963 function rssDirectMessageArray($message)
967 $from = $message->getFrom();
969 $entry['title'] = sprintf('Message from %1$s to %2$s',
970 $from->nickname, $message->getTo()->nickname);
972 $entry['content'] = common_xml_safe_str($message->rendered);
973 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
974 $entry['published'] = common_date_iso8601($message->created);
976 $taguribase = TagURI::base();
978 $entry['id'] = "tag:$taguribase:$entry[link]";
979 $entry['updated'] = $entry['published'];
981 $entry['author-name'] = $from->getBestName();
982 $entry['author-uri'] = $from->homepage;
984 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
986 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
987 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
991 $entry['description'] = $entry['content'];
992 $entry['pubDate'] = common_date_rfc2822($message->created);
993 $entry['guid'] = $entry['link'];
998 function showSingleXmlDirectMessage($message)
1000 $this->initDocument('xml');
1001 $dmsg = $this->directMessageArray($message);
1002 $this->showXmlDirectMessage($dmsg, true);
1003 $this->endDocument('xml');
1006 function showSingleJsonDirectMessage($message)
1008 $this->initDocument('json');
1009 $dmsg = $this->directMessageArray($message);
1010 $this->showJsonObjects($dmsg);
1011 $this->endDocument('json');
1014 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1016 $this->initDocument('atom');
1018 $this->element('title', null, common_xml_safe_str($title));
1019 $this->element('id', null, $id);
1020 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1022 if (!is_null($selfuri)) {
1023 $this->element('link', array('href' => $selfuri,
1024 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1027 $this->element('updated', null, common_date_iso8601('now'));
1028 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1030 if (is_array($group)) {
1031 foreach ($group as $g) {
1032 $this->raw($g->asAtomEntry());
1035 while ($group->fetch()) {
1036 $this->raw($group->asAtomEntry());
1040 $this->endDocument('atom');
1044 function showJsonTimeline($notice)
1046 $this->initDocument('json');
1048 $statuses = array();
1050 if (is_array($notice)) {
1051 $notice = new ArrayWrapper($notice);
1054 while ($notice->fetch()) {
1056 $twitter_status = $this->twitterStatusArray($notice);
1057 array_push($statuses, $twitter_status);
1058 } catch (Exception $e) {
1059 common_log(LOG_ERR, $e->getMessage());
1064 $this->showJsonObjects($statuses);
1066 $this->endDocument('json');
1069 function showJsonGroups($group)
1071 $this->initDocument('json');
1075 if (is_array($group)) {
1076 foreach ($group as $g) {
1077 $twitter_group = $this->twitterGroupArray($g);
1078 array_push($groups, $twitter_group);
1081 while ($group->fetch()) {
1082 $twitter_group = $this->twitterGroupArray($group);
1083 array_push($groups, $twitter_group);
1087 $this->showJsonObjects($groups);
1089 $this->endDocument('json');
1092 function showXmlGroups($group)
1095 $this->initDocument('xml');
1096 $this->elementStart('groups', array('type' => 'array'));
1098 if (is_array($group)) {
1099 foreach ($group as $g) {
1100 $twitter_group = $this->twitterGroupArray($g);
1101 $this->showTwitterXmlGroup($twitter_group);
1104 while ($group->fetch()) {
1105 $twitter_group = $this->twitterGroupArray($group);
1106 $this->showTwitterXmlGroup($twitter_group);
1110 $this->elementEnd('groups');
1111 $this->endDocument('xml');
1114 function showTwitterXmlUsers($user)
1116 $this->initDocument('xml');
1117 $this->elementStart('users', array('type' => 'array',
1118 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1120 if (is_array($user)) {
1121 foreach ($user as $u) {
1122 $twitter_user = $this->twitterUserArray($u);
1123 $this->showTwitterXmlUser($twitter_user);
1126 while ($user->fetch()) {
1127 $twitter_user = $this->twitterUserArray($user);
1128 $this->showTwitterXmlUser($twitter_user);
1132 $this->elementEnd('users');
1133 $this->endDocument('xml');
1136 function showJsonUsers($user)
1138 $this->initDocument('json');
1142 if (is_array($user)) {
1143 foreach ($user as $u) {
1144 $twitter_user = $this->twitterUserArray($u);
1145 array_push($users, $twitter_user);
1148 while ($user->fetch()) {
1149 $twitter_user = $this->twitterUserArray($user);
1150 array_push($users, $twitter_user);
1154 $this->showJsonObjects($users);
1156 $this->endDocument('json');
1159 function showSingleJsonGroup($group)
1161 $this->initDocument('json');
1162 $twitter_group = $this->twitterGroupArray($group);
1163 $this->showJsonObjects($twitter_group);
1164 $this->endDocument('json');
1167 function showSingleXmlGroup($group)
1169 $this->initDocument('xml');
1170 $twitter_group = $this->twitterGroupArray($group);
1171 $this->showTwitterXmlGroup($twitter_group);
1172 $this->endDocument('xml');
1175 function dateTwitter($dt)
1177 $dateStr = date('d F Y H:i:s', strtotime($dt));
1178 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1179 $d->setTimezone(new DateTimeZone(common_timezone()));
1180 return $d->format('D M d H:i:s O Y');
1183 function initDocument($type='xml')
1187 header('Content-Type: application/xml; charset=utf-8');
1191 header('Content-Type: application/json; charset=utf-8');
1193 // Check for JSONP callback
1194 if (isset($this->callback)) {
1195 print $this->callback . '(';
1199 header("Content-Type: application/rss+xml; charset=utf-8");
1200 $this->initTwitterRss();
1203 header('Content-Type: application/atom+xml; charset=utf-8');
1204 $this->initTwitterAtom();
1207 // TRANS: Client error on an API request with an unsupported data format.
1208 $this->clientError(_('Not a supported data format.'));
1215 function endDocument($type='xml')
1222 // Check for JSONP callback
1223 if (isset($this->callback)) {
1228 $this->endTwitterRss();
1231 $this->endTwitterRss();
1234 // TRANS: Client error on an API request with an unsupported data format.
1235 $this->clientError(_('Not a supported data format.'));
1241 function clientError($msg, $code = 400, $format = 'xml')
1243 $action = $this->trimmed('action');
1245 common_debug("User error '$code' on '$action': $msg", __FILE__);
1247 if (!array_key_exists($code, ClientErrorAction::$status)) {
1251 $status_string = ClientErrorAction::$status[$code];
1253 // Do not emit error header for JSONP
1254 if (!isset($this->callback)) {
1255 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1260 $this->initDocument('xml');
1261 $this->elementStart('hash');
1262 $this->element('error', null, $msg);
1263 $this->element('request', null, $_SERVER['REQUEST_URI']);
1264 $this->elementEnd('hash');
1265 $this->endDocument('xml');
1268 $this->initDocument('json');
1269 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1270 print(json_encode($error_array));
1271 $this->endDocument('json');
1274 header('Content-Type: text/plain; charset=utf-8');
1278 // If user didn't request a useful format, throw a regular client error
1279 throw new ClientException($msg, $code);
1283 function serverError($msg, $code = 500, $content_type = 'xml')
1285 $action = $this->trimmed('action');
1287 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1289 if (!array_key_exists($code, ServerErrorAction::$status)) {
1293 $status_string = ServerErrorAction::$status[$code];
1295 // Do not emit error header for JSONP
1296 if (!isset($this->callback)) {
1297 header('HTTP/1.1 '.$code.' '.$status_string);
1300 if ($content_type == 'xml') {
1301 $this->initDocument('xml');
1302 $this->elementStart('hash');
1303 $this->element('error', null, $msg);
1304 $this->element('request', null, $_SERVER['REQUEST_URI']);
1305 $this->elementEnd('hash');
1306 $this->endDocument('xml');
1308 $this->initDocument('json');
1309 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1310 print(json_encode($error_array));
1311 $this->endDocument('json');
1315 function initTwitterRss()
1318 $this->elementStart(
1322 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1323 'xmlns:georss' => 'http://www.georss.org/georss'
1326 $this->elementStart('channel');
1327 Event::handle('StartApiRss', array($this));
1330 function endTwitterRss()
1332 $this->elementEnd('channel');
1333 $this->elementEnd('rss');
1337 function initTwitterAtom()
1340 // FIXME: don't hardcode the language here!
1341 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1342 'xml:lang' => 'en-US',
1343 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1346 function endTwitterAtom()
1348 $this->elementEnd('feed');
1352 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1354 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1355 switch ($content_type) {
1357 $this->showTwitterXmlUser($profile_array);
1360 $this->showJsonObjects($profile_array);
1363 // TRANS: Client error on an API request with an unsupported data format.
1364 $this->clientError(_('Not a supported data format.'));
1370 private static function is_decimal($str)
1372 return preg_match('/^[0-9]+$/', $str);
1375 function getTargetUser($id)
1378 // Twitter supports these other ways of passing the user ID
1379 if (self::is_decimal($this->arg('id'))) {
1380 return User::staticGet($this->arg('id'));
1381 } else if ($this->arg('id')) {
1382 $nickname = common_canonical_nickname($this->arg('id'));
1383 return User::staticGet('nickname', $nickname);
1384 } else if ($this->arg('user_id')) {
1385 // This is to ensure that a non-numeric user_id still
1386 // overrides screen_name even if it doesn't get used
1387 if (self::is_decimal($this->arg('user_id'))) {
1388 return User::staticGet('id', $this->arg('user_id'));
1390 } else if ($this->arg('screen_name')) {
1391 $nickname = common_canonical_nickname($this->arg('screen_name'));
1392 return User::staticGet('nickname', $nickname);
1394 // Fall back to trying the currently authenticated user
1395 return $this->auth_user;
1398 } else if (self::is_decimal($id)) {
1399 return User::staticGet($id);
1401 $nickname = common_canonical_nickname($id);
1402 return User::staticGet('nickname', $nickname);
1406 function getTargetProfile($id)
1410 // Twitter supports these other ways of passing the user ID
1411 if (self::is_decimal($this->arg('id'))) {
1412 return Profile::staticGet($this->arg('id'));
1413 } else if ($this->arg('id')) {
1414 // Screen names currently can only uniquely identify a local user.
1415 $nickname = common_canonical_nickname($this->arg('id'));
1416 $user = User::staticGet('nickname', $nickname);
1417 return $user ? $user->getProfile() : null;
1418 } else if ($this->arg('user_id')) {
1419 // This is to ensure that a non-numeric user_id still
1420 // overrides screen_name even if it doesn't get used
1421 if (self::is_decimal($this->arg('user_id'))) {
1422 return Profile::staticGet('id', $this->arg('user_id'));
1424 } else if ($this->arg('screen_name')) {
1425 $nickname = common_canonical_nickname($this->arg('screen_name'));
1426 $user = User::staticGet('nickname', $nickname);
1427 return $user ? $user->getProfile() : null;
1429 } else if (self::is_decimal($id)) {
1430 return Profile::staticGet($id);
1432 $nickname = common_canonical_nickname($id);
1433 $user = User::staticGet('nickname', $nickname);
1434 return $user ? $user->getProfile() : null;
1438 function getTargetGroup($id)
1441 if (self::is_decimal($this->arg('id'))) {
1442 return User_group::staticGet($this->arg('id'));
1443 } else if ($this->arg('id')) {
1444 $nickname = common_canonical_nickname($this->arg('id'));
1445 $local = Local_group::staticGet('nickname', $nickname);
1446 if (empty($local)) {
1449 return User_group::staticGet('id', $local->id);
1451 } else if ($this->arg('group_id')) {
1452 // This is to ensure that a non-numeric user_id still
1453 // overrides screen_name even if it doesn't get used
1454 if (self::is_decimal($this->arg('group_id'))) {
1455 return User_group::staticGet('id', $this->arg('group_id'));
1457 } else if ($this->arg('group_name')) {
1458 $nickname = common_canonical_nickname($this->arg('group_name'));
1459 $local = Local_group::staticGet('nickname', $nickname);
1460 if (empty($local)) {
1463 return User_group::staticGet('id', $local->group_id);
1467 } else if (self::is_decimal($id)) {
1468 return User_group::staticGet($id);
1470 $nickname = common_canonical_nickname($id);
1471 $local = Local_group::staticGet('nickname', $nickname);
1472 if (empty($local)) {
1475 return User_group::staticGet('id', $local->group_id);
1481 * Returns query argument or default value if not found. Certain
1482 * parameters used throughout the API are lightly scrubbed and
1483 * bounds checked. This overrides Action::arg().
1485 * @param string $key requested argument
1486 * @param string $def default value to return if $key is not provided
1490 function arg($key, $def=null)
1492 // XXX: Do even more input validation/scrubbing?
1494 if (array_key_exists($key, $this->args)) {
1497 $page = (int)$this->args['page'];
1498 return ($page < 1) ? 1 : $page;
1500 $count = (int)$this->args['count'];
1503 } elseif ($count > 200) {
1509 $since_id = (int)$this->args['since_id'];
1510 return ($since_id < 1) ? 0 : $since_id;
1512 $max_id = (int)$this->args['max_id'];
1513 return ($max_id < 1) ? 0 : $max_id;
1515 return parent::arg($key, $def);
1523 * Calculate the complete URI that called up this action. Used for
1524 * Atom rel="self" links. Warning: this is funky.
1526 * @return string URL a URL suitable for rel="self" Atom links
1528 function getSelfUri()
1530 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1532 $id = $this->arg('id');
1533 $aargs = array('format' => $this->format);
1538 $tag = $this->arg('tag');
1540 $aargs['tag'] = $tag;
1543 parse_str($_SERVER['QUERY_STRING'], $params);
1545 if (!empty($params)) {
1546 unset($params['p']);
1547 $pstring = http_build_query($params);
1550 $uri = common_local_url($action, $aargs);
1552 if (!empty($pstring)) {
1553 $uri .= '?' . $pstring;