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')) {
102 * Contains most of the Twitter-compatible API output functions.
106 * @author Craig Andrews <candrews@integralblue.com>
107 * @author Dan Moore <dan@moore.cx>
108 * @author Evan Prodromou <evan@status.net>
109 * @author Jeffery To <jeffery.to@gmail.com>
110 * @author Toby Inkster <mail@tobyinkster.co.uk>
111 * @author Zach Copley <zach@status.net>
112 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
113 * @link http://status.net/
116 class ApiAction extends Action
119 const READ_WRITE = 2;
123 var $auth_user = null;
127 var $since_id = null;
129 var $callback = null;
131 var $access = self::READ_ONLY; // read (default) or read-write
133 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
138 * @param array $args Web and URL arguments
140 * @return boolean false if user doesn't exist
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
176 function handle($args)
178 header('Access-Control-Allow-Origin: *');
179 parent::handle($args);
183 * Overrides XMLOutputter::element to write booleans as strings (true|false).
184 * See that method's documentation for more info.
186 * @param string $tag Element type or tagname
187 * @param array $attrs Array of element attributes, as
189 * @param string $content string content of the element
193 function element($tag, $attrs=null, $content=null)
195 if (is_bool($content)) {
196 $content = ($content ? 'true' : 'false');
199 return parent::element($tag, $attrs, $content);
202 function twitterUserArray($profile, $get_notice=false)
204 $twitter_user = array();
206 $twitter_user['id'] = intval($profile->id);
207 $twitter_user['name'] = $profile->getBestName();
208 $twitter_user['screen_name'] = $profile->nickname;
209 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
210 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
212 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
213 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
214 Avatar::defaultImage(AVATAR_STREAM_SIZE);
216 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
217 $twitter_user['protected'] = false; # not supported by StatusNet yet
218 $twitter_user['followers_count'] = $profile->subscriberCount();
221 $user = $profile->getUser();
223 // Note: some profiles don't have an associated user
225 $defaultDesign = Design::siteDesign();
228 $design = $user->getDesign();
231 if (empty($design)) {
232 $design = $defaultDesign;
235 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
236 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
237 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
238 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
240 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
242 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243 $twitter_user['profile_sidebar_border_color'] = '';
245 $twitter_user['friends_count'] = $profile->subscriptionCount();
247 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
249 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
253 if (!empty($user) && $user->timezone) {
254 $timezone = $user->timezone;
258 $t->setTimezone(new DateTimeZone($timezone));
260 $twitter_user['utc_offset'] = $t->format('Z');
261 $twitter_user['time_zone'] = $timezone;
263 $twitter_user['profile_background_image_url']
264 = empty($design->backgroundimage)
265 ? '' : ($design->disposition & BACKGROUND_ON)
266 ? Design::url($design->backgroundimage) : '';
268 $twitter_user['profile_background_tile']
269 = empty($design->disposition)
270 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
272 $twitter_user['statuses_count'] = $profile->noticeCount();
274 // Is the requesting user following this user?
275 $twitter_user['following'] = false;
276 $twitter_user['statusnet:blocking'] = false;
277 $twitter_user['notifications'] = false;
279 if (isset($this->auth_user)) {
281 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
282 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
285 $sub = Subscription::pkeyGet(array('subscriber' =>
286 $this->auth_user->id,
287 'subscribed' => $profile->id));
290 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
295 $notice = $profile->getCurrentNotice();
298 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
302 // StatusNet-specific
304 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
306 return $twitter_user;
309 function twitterStatusArray($notice, $include_user=true)
311 $base = $this->twitterSimpleStatusArray($notice, $include_user);
313 if (!empty($notice->repeat_of)) {
314 $original = Notice::staticGet('id', $notice->repeat_of);
315 if (!empty($original)) {
316 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
317 $base['retweeted_status'] = $original_array;
324 function twitterSimpleStatusArray($notice, $include_user=true)
326 $profile = $notice->getProfile();
328 $twitter_status = array();
329 $twitter_status['text'] = $notice->content;
330 $twitter_status['truncated'] = false; # Not possible on StatusNet
331 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
332 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
333 intval($notice->reply_to) : null;
337 $ns = $notice->getSource();
339 if (!empty($ns->name) && !empty($ns->url)) {
340 $source = '<a href="'
341 . htmlspecialchars($ns->url)
342 . '" rel="nofollow">'
343 . htmlspecialchars($ns->name)
350 $twitter_status['source'] = $source;
351 $twitter_status['id'] = intval($notice->id);
353 $replier_profile = null;
355 if ($notice->reply_to) {
356 $reply = Notice::staticGet(intval($notice->reply_to));
358 $replier_profile = $reply->getProfile();
362 $twitter_status['in_reply_to_user_id'] =
363 ($replier_profile) ? intval($replier_profile->id) : null;
364 $twitter_status['in_reply_to_screen_name'] =
365 ($replier_profile) ? $replier_profile->nickname : null;
367 if (isset($notice->lat) && isset($notice->lon)) {
368 // This is the format that GeoJSON expects stuff to be in
369 $twitter_status['geo'] = array('type' => 'Point',
370 'coordinates' => array((float) $notice->lat,
371 (float) $notice->lon));
373 $twitter_status['geo'] = null;
376 if (isset($this->auth_user)) {
377 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
379 $twitter_status['favorited'] = false;
383 $attachments = $notice->attachments();
385 if (!empty($attachments)) {
387 $twitter_status['attachments'] = array();
389 foreach ($attachments as $attachment) {
390 $enclosure_o=$attachment->getEnclosure();
392 $enclosure = array();
393 $enclosure['url'] = $enclosure_o->url;
394 $enclosure['mimetype'] = $enclosure_o->mimetype;
395 $enclosure['size'] = $enclosure_o->size;
396 $twitter_status['attachments'][] = $enclosure;
401 if ($include_user && $profile) {
402 # Don't get notice (recursive!)
403 $twitter_user = $this->twitterUserArray($profile, false);
404 $twitter_status['user'] = $twitter_user;
407 // StatusNet-specific
409 $twitter_status['statusnet_html'] = $notice->rendered;
411 return $twitter_status;
414 function twitterGroupArray($group)
416 $twitter_group = array();
418 $twitter_group['id'] = $group->id;
419 $twitter_group['url'] = $group->permalink();
420 $twitter_group['nickname'] = $group->nickname;
421 $twitter_group['fullname'] = $group->fullname;
423 if (isset($this->auth_user)) {
424 $twitter_group['member'] = $this->auth_user->isMember($group);
425 $twitter_group['blocked'] = Group_block::isBlocked(
427 $this->auth_user->getProfile()
431 $twitter_group['member_count'] = $group->getMemberCount();
432 $twitter_group['original_logo'] = $group->original_logo;
433 $twitter_group['homepage_logo'] = $group->homepage_logo;
434 $twitter_group['stream_logo'] = $group->stream_logo;
435 $twitter_group['mini_logo'] = $group->mini_logo;
436 $twitter_group['homepage'] = $group->homepage;
437 $twitter_group['description'] = $group->description;
438 $twitter_group['location'] = $group->location;
439 $twitter_group['created'] = $this->dateTwitter($group->created);
440 $twitter_group['modified'] = $this->dateTwitter($group->modified);
442 return $twitter_group;
445 function twitterRssGroupArray($group)
448 $entry['content']=$group->description;
449 $entry['title']=$group->nickname;
450 $entry['link']=$group->permalink();
451 $entry['published']=common_date_iso8601($group->created);
452 $entry['updated']==common_date_iso8601($group->modified);
453 $taguribase = common_config('integration', 'groupuri');
454 $entry['id'] = "group:$groupuribase:$entry[link]";
456 $entry['description'] = $entry['content'];
457 $entry['pubDate'] = common_date_rfc2822($group->created);
458 $entry['guid'] = $entry['link'];
463 function twitterRssEntryArray($notice)
467 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
469 $profile = $notice->getProfile();
471 // We trim() to avoid extraneous whitespace in the output
473 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
474 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
475 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
476 $entry['published'] = common_date_iso8601($notice->created);
478 $taguribase = TagURI::base();
479 $entry['id'] = "tag:$taguribase:$entry[link]";
481 $entry['updated'] = $entry['published'];
482 $entry['author'] = $profile->getBestName();
485 $attachments = $notice->attachments();
486 $enclosures = array();
488 foreach ($attachments as $attachment) {
489 $enclosure_o=$attachment->getEnclosure();
491 $enclosure = array();
492 $enclosure['url'] = $enclosure_o->url;
493 $enclosure['mimetype'] = $enclosure_o->mimetype;
494 $enclosure['size'] = $enclosure_o->size;
495 $enclosures[] = $enclosure;
499 if (!empty($enclosures)) {
500 $entry['enclosures'] = $enclosures;
504 $tag = new Notice_tag();
505 $tag->notice_id = $notice->id;
507 $entry['tags']=array();
508 while ($tag->fetch()) {
509 $entry['tags'][]=$tag->tag;
515 $entry['description'] = $entry['content'];
516 $entry['pubDate'] = common_date_rfc2822($notice->created);
517 $entry['guid'] = $entry['link'];
519 if (isset($notice->lat) && isset($notice->lon)) {
520 // This is the format that GeoJSON expects stuff to be in.
521 // showGeoRSS() below uses it for XML output, so we reuse it
522 $entry['geo'] = array('type' => 'Point',
523 'coordinates' => array((float) $notice->lat,
524 (float) $notice->lon));
526 $entry['geo'] = null;
529 Event::handle('EndRssEntryArray', array($notice, &$entry));
535 function twitterRelationshipArray($source, $target)
537 $relationship = array();
539 $relationship['source'] =
540 $this->relationshipDetailsArray($source, $target);
541 $relationship['target'] =
542 $this->relationshipDetailsArray($target, $source);
544 return array('relationship' => $relationship);
547 function relationshipDetailsArray($source, $target)
551 $details['screen_name'] = $source->nickname;
552 $details['followed_by'] = $target->isSubscribed($source);
553 $details['following'] = $source->isSubscribed($target);
555 $notifications = false;
557 if ($source->isSubscribed($target)) {
559 $sub = Subscription::pkeyGet(array('subscriber' =>
560 $source->id, 'subscribed' => $target->id));
563 $notifications = ($sub->jabber || $sub->sms);
567 $details['notifications_enabled'] = $notifications;
568 $details['blocking'] = $source->hasBlocked($target);
569 $details['id'] = $source->id;
574 function showTwitterXmlRelationship($relationship)
576 $this->elementStart('relationship');
578 foreach($relationship as $element => $value) {
579 if ($element == 'source' || $element == 'target') {
580 $this->elementStart($element);
581 $this->showXmlRelationshipDetails($value);
582 $this->elementEnd($element);
586 $this->elementEnd('relationship');
589 function showXmlRelationshipDetails($details)
591 foreach($details as $element => $value) {
592 $this->element($element, null, $value);
596 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
600 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
602 $this->elementStart($tag, $attrs);
603 foreach($twitter_status as $element => $value) {
606 $this->showTwitterXmlUser($twitter_status['user']);
609 $this->element($element, null, common_xml_safe_str($value));
612 $this->showXmlAttachments($twitter_status['attachments']);
615 $this->showGeoXML($value);
617 case 'retweeted_status':
618 $this->showTwitterXmlStatus($value, 'retweeted_status');
621 if (strncmp($element, 'statusnet_', 10) == 0) {
622 $this->element('statusnet:'.substr($element, 10), null, $value);
624 $this->element($element, null, $value);
628 $this->elementEnd($tag);
631 function showTwitterXmlGroup($twitter_group)
633 $this->elementStart('group');
634 foreach($twitter_group as $element => $value) {
635 $this->element($element, null, $value);
637 $this->elementEnd('group');
640 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
644 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
646 $this->elementStart($role, $attrs);
647 foreach($twitter_user as $element => $value) {
648 if ($element == 'status') {
649 $this->showTwitterXmlStatus($twitter_user['status']);
650 } else if (strncmp($element, 'statusnet_', 10) == 0) {
651 $this->element('statusnet:'.substr($element, 10), null, $value);
653 $this->element($element, null, $value);
656 $this->elementEnd($role);
659 function showXmlAttachments($attachments) {
660 if (!empty($attachments)) {
661 $this->elementStart('attachments', array('type' => 'array'));
662 foreach ($attachments as $attachment) {
664 $attrs['url'] = $attachment['url'];
665 $attrs['mimetype'] = $attachment['mimetype'];
666 $attrs['size'] = $attachment['size'];
667 $this->element('enclosure', $attrs, '');
669 $this->elementEnd('attachments');
673 function showGeoXML($geo)
677 $this->element('geo');
679 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
680 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
681 $this->elementEnd('geo');
685 function showGeoRSS($geo)
691 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
696 function showTwitterRssItem($entry)
698 $this->elementStart('item');
699 $this->element('title', null, $entry['title']);
700 $this->element('description', null, $entry['description']);
701 $this->element('pubDate', null, $entry['pubDate']);
702 $this->element('guid', null, $entry['guid']);
703 $this->element('link', null, $entry['link']);
705 # RSS only supports 1 enclosure per item
706 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
707 $enclosure = $entry['enclosures'][0];
708 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
711 if(array_key_exists('tags', $entry)){
712 foreach($entry['tags'] as $tag){
713 $this->element('category', null,$tag);
717 $this->showGeoRSS($entry['geo']);
718 $this->elementEnd('item');
721 function showJsonObjects($objects)
723 print(json_encode($objects));
726 function showSingleXmlStatus($notice)
728 $this->initDocument('xml');
729 $twitter_status = $this->twitterStatusArray($notice);
730 $this->showTwitterXmlStatus($twitter_status, 'status', true);
731 $this->endDocument('xml');
734 function show_single_json_status($notice)
736 $this->initDocument('json');
737 $status = $this->twitterStatusArray($notice);
738 $this->showJsonObjects($status);
739 $this->endDocument('json');
742 function showXmlTimeline($notice)
745 $this->initDocument('xml');
746 $this->elementStart('statuses', array('type' => 'array',
747 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
749 if (is_array($notice)) {
750 $notice = new ArrayWrapper($notice);
753 while ($notice->fetch()) {
755 $twitter_status = $this->twitterStatusArray($notice);
756 $this->showTwitterXmlStatus($twitter_status);
757 } catch (Exception $e) {
758 common_log(LOG_ERR, $e->getMessage());
763 $this->elementEnd('statuses');
764 $this->endDocument('xml');
767 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)
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');
868 function showRssGroups($group, $title, $link, $subtitle)
871 $this->initDocument('rss');
873 $this->element('title', null, $title);
874 $this->element('link', null, $link);
875 $this->element('description', null, $subtitle);
876 $this->element('language', null, 'en-us');
877 $this->element('ttl', null, '40');
879 if (is_array($group)) {
880 foreach ($group as $g) {
881 $twitter_group = $this->twitterRssGroupArray($g);
882 $this->showTwitterRssItem($twitter_group);
885 while ($group->fetch()) {
886 $twitter_group = $this->twitterRssGroupArray($group);
887 $this->showTwitterRssItem($twitter_group);
891 $this->endTwitterRss();
894 function showTwitterAtomEntry($entry)
896 $this->elementStart('entry');
897 $this->element('title', null, common_xml_safe_str($entry['title']));
900 array('type' => 'html'),
901 common_xml_safe_str($entry['content'])
903 $this->element('id', null, $entry['id']);
904 $this->element('published', null, $entry['published']);
905 $this->element('updated', null, $entry['updated']);
906 $this->element('link', array('type' => 'text/html',
907 'href' => $entry['link'],
908 'rel' => 'alternate'));
909 $this->element('link', array('type' => $entry['avatar-type'],
910 'href' => $entry['avatar'],
912 $this->elementStart('author');
914 $this->element('name', null, $entry['author-name']);
915 $this->element('uri', null, $entry['author-uri']);
917 $this->elementEnd('author');
918 $this->elementEnd('entry');
921 function showXmlDirectMessage($dm, $namespaces=false)
925 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
927 $this->elementStart('direct_message', $attrs);
928 foreach($dm as $element => $value) {
932 $this->showTwitterXmlUser($value, $element);
935 $this->element($element, null, common_xml_safe_str($value));
938 $this->element($element, null, $value);
942 $this->elementEnd('direct_message');
945 function directMessageArray($message)
949 $from_profile = $message->getFrom();
950 $to_profile = $message->getTo();
952 $dmsg['id'] = $message->id;
953 $dmsg['sender_id'] = $message->from_profile;
954 $dmsg['text'] = trim($message->content);
955 $dmsg['recipient_id'] = $message->to_profile;
956 $dmsg['created_at'] = $this->dateTwitter($message->created);
957 $dmsg['sender_screen_name'] = $from_profile->nickname;
958 $dmsg['recipient_screen_name'] = $to_profile->nickname;
959 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
960 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
965 function rssDirectMessageArray($message)
969 $from = $message->getFrom();
971 $entry['title'] = sprintf('Message from %1$s to %2$s',
972 $from->nickname, $message->getTo()->nickname);
974 $entry['content'] = common_xml_safe_str($message->rendered);
975 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
976 $entry['published'] = common_date_iso8601($message->created);
978 $taguribase = TagURI::base();
980 $entry['id'] = "tag:$taguribase:$entry[link]";
981 $entry['updated'] = $entry['published'];
983 $entry['author-name'] = $from->getBestName();
984 $entry['author-uri'] = $from->homepage;
986 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
988 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
989 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
993 $entry['description'] = $entry['content'];
994 $entry['pubDate'] = common_date_rfc2822($message->created);
995 $entry['guid'] = $entry['link'];
1000 function showSingleXmlDirectMessage($message)
1002 $this->initDocument('xml');
1003 $dmsg = $this->directMessageArray($message);
1004 $this->showXmlDirectMessage($dmsg, true);
1005 $this->endDocument('xml');
1008 function showSingleJsonDirectMessage($message)
1010 $this->initDocument('json');
1011 $dmsg = $this->directMessageArray($message);
1012 $this->showJsonObjects($dmsg);
1013 $this->endDocument('json');
1016 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1019 $this->initDocument('atom');
1021 $this->element('title', null, common_xml_safe_str($title));
1022 $this->element('id', null, $id);
1023 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1025 if (!is_null($selfuri)) {
1026 $this->element('link', array('href' => $selfuri,
1027 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1030 $this->element('updated', null, common_date_iso8601('now'));
1031 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1033 if (is_array($group)) {
1034 foreach ($group as $g) {
1035 $this->raw($g->asAtomEntry());
1038 while ($group->fetch()) {
1039 $this->raw($group->asAtomEntry());
1043 $this->endDocument('atom');
1047 function showJsonTimeline($notice)
1050 $this->initDocument('json');
1052 $statuses = array();
1054 if (is_array($notice)) {
1055 $notice = new ArrayWrapper($notice);
1058 while ($notice->fetch()) {
1060 $twitter_status = $this->twitterStatusArray($notice);
1061 array_push($statuses, $twitter_status);
1062 } catch (Exception $e) {
1063 common_log(LOG_ERR, $e->getMessage());
1068 $this->showJsonObjects($statuses);
1070 $this->endDocument('json');
1073 function showJsonGroups($group)
1076 $this->initDocument('json');
1080 if (is_array($group)) {
1081 foreach ($group as $g) {
1082 $twitter_group = $this->twitterGroupArray($g);
1083 array_push($groups, $twitter_group);
1086 while ($group->fetch()) {
1087 $twitter_group = $this->twitterGroupArray($group);
1088 array_push($groups, $twitter_group);
1092 $this->showJsonObjects($groups);
1094 $this->endDocument('json');
1097 function showXmlGroups($group)
1100 $this->initDocument('xml');
1101 $this->elementStart('groups', array('type' => 'array'));
1103 if (is_array($group)) {
1104 foreach ($group as $g) {
1105 $twitter_group = $this->twitterGroupArray($g);
1106 $this->showTwitterXmlGroup($twitter_group);
1109 while ($group->fetch()) {
1110 $twitter_group = $this->twitterGroupArray($group);
1111 $this->showTwitterXmlGroup($twitter_group);
1115 $this->elementEnd('groups');
1116 $this->endDocument('xml');
1119 function showTwitterXmlUsers($user)
1122 $this->initDocument('xml');
1123 $this->elementStart('users', array('type' => 'array',
1124 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1126 if (is_array($user)) {
1127 foreach ($user as $u) {
1128 $twitter_user = $this->twitterUserArray($u);
1129 $this->showTwitterXmlUser($twitter_user);
1132 while ($user->fetch()) {
1133 $twitter_user = $this->twitterUserArray($user);
1134 $this->showTwitterXmlUser($twitter_user);
1138 $this->elementEnd('users');
1139 $this->endDocument('xml');
1142 function showJsonUsers($user)
1145 $this->initDocument('json');
1149 if (is_array($user)) {
1150 foreach ($user as $u) {
1151 $twitter_user = $this->twitterUserArray($u);
1152 array_push($users, $twitter_user);
1155 while ($user->fetch()) {
1156 $twitter_user = $this->twitterUserArray($user);
1157 array_push($users, $twitter_user);
1161 $this->showJsonObjects($users);
1163 $this->endDocument('json');
1166 function showSingleJsonGroup($group)
1168 $this->initDocument('json');
1169 $twitter_group = $this->twitterGroupArray($group);
1170 $this->showJsonObjects($twitter_group);
1171 $this->endDocument('json');
1174 function showSingleXmlGroup($group)
1176 $this->initDocument('xml');
1177 $twitter_group = $this->twitterGroupArray($group);
1178 $this->showTwitterXmlGroup($twitter_group);
1179 $this->endDocument('xml');
1182 function dateTwitter($dt)
1184 $dateStr = date('d F Y H:i:s', strtotime($dt));
1185 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1186 $d->setTimezone(new DateTimeZone(common_timezone()));
1187 return $d->format('D M d H:i:s O Y');
1190 function initDocument($type='xml')
1194 header('Content-Type: application/xml; charset=utf-8');
1198 header('Content-Type: application/json; charset=utf-8');
1200 // Check for JSONP callback
1201 if (isset($this->callback)) {
1202 print $this->callback . '(';
1206 header("Content-Type: application/rss+xml; charset=utf-8");
1207 $this->initTwitterRss();
1210 header('Content-Type: application/atom+xml; charset=utf-8');
1211 $this->initTwitterAtom();
1214 // TRANS: Client error on an API request with an unsupported data format.
1215 $this->clientError(_('Not a supported data format.'));
1222 function endDocument($type='xml')
1230 // Check for JSONP callback
1231 if (isset($this->callback)) {
1236 $this->endTwitterRss();
1239 $this->endTwitterRss();
1242 // TRANS: Client error on an API request with an unsupported data format.
1243 $this->clientError(_('Not a supported data format.'));
1249 function clientError($msg, $code = 400, $format = 'xml')
1251 $action = $this->trimmed('action');
1253 common_debug("User error '$code' on '$action': $msg", __FILE__);
1255 if (!array_key_exists($code, ClientErrorAction::$status)) {
1259 $status_string = ClientErrorAction::$status[$code];
1261 // Do not emit error header for JSONP
1262 if (!isset($this->callback)) {
1263 header('HTTP/1.1 '.$code.' '.$status_string);
1266 if ($format == 'xml') {
1267 $this->initDocument('xml');
1268 $this->elementStart('hash');
1269 $this->element('error', null, $msg);
1270 $this->element('request', null, $_SERVER['REQUEST_URI']);
1271 $this->elementEnd('hash');
1272 $this->endDocument('xml');
1273 } elseif ($format == 'json'){
1274 $this->initDocument('json');
1275 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1276 print(json_encode($error_array));
1277 $this->endDocument('json');
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 = 'xml')
1287 $action = $this->trimmed('action');
1289 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1291 if (!array_key_exists($code, ServerErrorAction::$status)) {
1295 $status_string = ServerErrorAction::$status[$code];
1297 // Do not emit error header for JSONP
1298 if (!isset($this->callback)) {
1299 header('HTTP/1.1 '.$code.' '.$status_string);
1302 if ($content_type == 'xml') {
1303 $this->initDocument('xml');
1304 $this->elementStart('hash');
1305 $this->element('error', null, $msg);
1306 $this->element('request', null, $_SERVER['REQUEST_URI']);
1307 $this->elementEnd('hash');
1308 $this->endDocument('xml');
1310 $this->initDocument('json');
1311 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1312 print(json_encode($error_array));
1313 $this->endDocument('json');
1317 function initTwitterRss()
1320 $this->elementStart(
1324 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1325 'xmlns:georss' => 'http://www.georss.org/georss'
1328 $this->elementStart('channel');
1329 Event::handle('StartApiRss', array($this));
1332 function endTwitterRss()
1334 $this->elementEnd('channel');
1335 $this->elementEnd('rss');
1339 function initTwitterAtom()
1342 // FIXME: don't hardcode the language here!
1343 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1344 'xml:lang' => 'en-US',
1345 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1348 function endTwitterAtom()
1350 $this->elementEnd('feed');
1354 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1356 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1357 switch ($content_type) {
1359 $this->showTwitterXmlUser($profile_array);
1362 $this->showJsonObjects($profile_array);
1365 // TRANS: Client error on an API request with an unsupported data format.
1366 $this->clientError(_('Not a supported data format.'));
1372 function getTargetUser($id)
1376 // Twitter supports these other ways of passing the user ID
1377 if (is_numeric($this->arg('id'))) {
1378 return User::staticGet($this->arg('id'));
1379 } else if ($this->arg('id')) {
1380 $nickname = common_canonical_nickname($this->arg('id'));
1381 return User::staticGet('nickname', $nickname);
1382 } else if ($this->arg('user_id')) {
1383 // This is to ensure that a non-numeric user_id still
1384 // overrides screen_name even if it doesn't get used
1385 if (is_numeric($this->arg('user_id'))) {
1386 return User::staticGet('id', $this->arg('user_id'));
1388 } else if ($this->arg('screen_name')) {
1389 $nickname = common_canonical_nickname($this->arg('screen_name'));
1390 return User::staticGet('nickname', $nickname);
1392 // Fall back to trying the currently authenticated user
1393 return $this->auth_user;
1396 } else if (is_numeric($id)) {
1397 return User::staticGet($id);
1399 $nickname = common_canonical_nickname($id);
1400 return User::staticGet('nickname', $nickname);
1404 function getTargetProfile($id)
1408 // Twitter supports these other ways of passing the user ID
1409 if (is_numeric($this->arg('id'))) {
1410 return Profile::staticGet($this->arg('id'));
1411 } else if ($this->arg('id')) {
1412 $nickname = common_canonical_nickname($this->arg('id'));
1413 return Profile::staticGet('nickname', $nickname);
1414 } else if ($this->arg('user_id')) {
1415 // This is to ensure that a non-numeric user_id still
1416 // overrides screen_name even if it doesn't get used
1417 if (is_numeric($this->arg('user_id'))) {
1418 return Profile::staticGet('id', $this->arg('user_id'));
1420 } else if ($this->arg('screen_name')) {
1421 $nickname = common_canonical_nickname($this->arg('screen_name'));
1422 return Profile::staticGet('nickname', $nickname);
1424 } else if (is_numeric($id)) {
1425 return Profile::staticGet($id);
1427 $nickname = common_canonical_nickname($id);
1428 return Profile::staticGet('nickname', $nickname);
1432 function getTargetGroup($id)
1435 if (is_numeric($this->arg('id'))) {
1436 return User_group::staticGet($this->arg('id'));
1437 } else if ($this->arg('id')) {
1438 $nickname = common_canonical_nickname($this->arg('id'));
1439 $local = Local_group::staticGet('nickname', $nickname);
1440 if (empty($local)) {
1443 return User_group::staticGet('id', $local->id);
1445 } else if ($this->arg('group_id')) {
1446 // This is to ensure that a non-numeric user_id still
1447 // overrides screen_name even if it doesn't get used
1448 if (is_numeric($this->arg('group_id'))) {
1449 return User_group::staticGet('id', $this->arg('group_id'));
1451 } else if ($this->arg('group_name')) {
1452 $nickname = common_canonical_nickname($this->arg('group_name'));
1453 $local = Local_group::staticGet('nickname', $nickname);
1454 if (empty($local)) {
1457 return User_group::staticGet('id', $local->group_id);
1461 } else if (is_numeric($id)) {
1462 return User_group::staticGet($id);
1464 $nickname = common_canonical_nickname($id);
1465 $local = Local_group::staticGet('nickname', $nickname);
1466 if (empty($local)) {
1469 return User_group::staticGet('id', $local->group_id);
1475 * Returns query argument or default value if not found. Certain
1476 * parameters used throughout the API are lightly scrubbed and
1477 * bounds checked. This overrides Action::arg().
1479 * @param string $key requested argument
1480 * @param string $def default value to return if $key is not provided
1484 function arg($key, $def=null)
1487 // XXX: Do even more input validation/scrubbing?
1489 if (array_key_exists($key, $this->args)) {
1492 $page = (int)$this->args['page'];
1493 return ($page < 1) ? 1 : $page;
1495 $count = (int)$this->args['count'];
1498 } elseif ($count > 200) {
1504 $since_id = (int)$this->args['since_id'];
1505 return ($since_id < 1) ? 0 : $since_id;
1507 $max_id = (int)$this->args['max_id'];
1508 return ($max_id < 1) ? 0 : $max_id;
1510 return parent::arg($key, $def);
1518 * Calculate the complete URI that called up this action. Used for
1519 * Atom rel="self" links. Warning: this is funky.
1521 * @return string URL a URL suitable for rel="self" Atom links
1523 function getSelfUri()
1525 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1527 $id = $this->arg('id');
1528 $aargs = array('format' => $this->format);
1533 $tag = $this->arg('tag');
1535 $aargs['tag'] = $tag;
1538 parse_str($_SERVER['QUERY_STRING'], $params);
1540 if (!empty($params)) {
1541 unset($params['p']);
1542 $pstring = http_build_query($params);
1545 $uri = common_local_url($action, $aargs);
1547 if (!empty($pstring)) {
1548 $uri .= '?' . $pstring;