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/
118 class ApiAction extends Action
121 const READ_WRITE = 2;
125 var $auth_user = null;
129 var $since_id = null;
131 var $callback = null;
133 var $access = self::READ_ONLY; // read (default) or read-write
135 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
140 * @param array $args Web and URL arguments
142 * @return boolean false if user doesn't exist
145 function prepare($args)
147 StatusNet::setApi(true); // reduce exception reports to aid in debugging
148 parent::prepare($args);
150 $this->format = $this->arg('format');
151 $this->callback = $this->arg('callback');
152 $this->page = (int)$this->arg('page', 1);
153 $this->count = (int)$this->arg('count', 20);
154 $this->max_id = (int)$this->arg('max_id', 0);
155 $this->since_id = (int)$this->arg('since_id', 0);
157 if ($this->arg('since')) {
158 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
161 $this->source = $this->trimmed('source');
163 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
164 $this->source = 'api';
173 * @param array $args Arguments from $_REQUEST
178 function handle($args)
180 header('Access-Control-Allow-Origin: *');
181 parent::handle($args);
185 * Overrides XMLOutputter::element to write booleans as strings (true|false).
186 * See that method's documentation for more info.
188 * @param string $tag Element type or tagname
189 * @param array $attrs Array of element attributes, as
191 * @param string $content string content of the element
195 function element($tag, $attrs=null, $content=null)
197 if (is_bool($content)) {
198 $content = ($content ? 'true' : 'false');
201 return parent::element($tag, $attrs, $content);
204 function twitterUserArray($profile, $get_notice=false)
206 $twitter_user = array();
208 $twitter_user['id'] = intval($profile->id);
209 $twitter_user['name'] = $profile->getBestName();
210 $twitter_user['screen_name'] = $profile->nickname;
211 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
212 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
214 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
215 $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
216 Avatar::defaultImage(AVATAR_STREAM_SIZE);
218 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
219 $twitter_user['protected'] = false; # not supported by StatusNet yet
220 $twitter_user['followers_count'] = $profile->subscriberCount();
223 $user = $profile->getUser();
225 // Note: some profiles don't have an associated user
227 $defaultDesign = Design::siteDesign();
230 $design = $user->getDesign();
233 if (empty($design)) {
234 $design = $defaultDesign;
237 $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
238 $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
239 $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
240 $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
241 $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
242 $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
243 $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
244 $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
245 $twitter_user['profile_sidebar_border_color'] = '';
247 $twitter_user['friends_count'] = $profile->subscriptionCount();
249 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
251 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
255 if (!empty($user) && $user->timezone) {
256 $timezone = $user->timezone;
260 $t->setTimezone(new DateTimeZone($timezone));
262 $twitter_user['utc_offset'] = $t->format('Z');
263 $twitter_user['time_zone'] = $timezone;
265 $twitter_user['profile_background_image_url']
266 = empty($design->backgroundimage)
267 ? '' : ($design->disposition & BACKGROUND_ON)
268 ? Design::url($design->backgroundimage) : '';
270 $twitter_user['profile_background_tile']
271 = empty($design->disposition)
272 ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
274 $twitter_user['statuses_count'] = $profile->noticeCount();
276 // Is the requesting user following this user?
277 $twitter_user['following'] = false;
278 $twitter_user['statusnet:blocking'] = false;
279 $twitter_user['notifications'] = false;
281 if (isset($this->auth_user)) {
283 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
284 $twitter_user['statusnet:blocking'] = $this->auth_user->hasBlocked($profile);
287 $sub = Subscription::pkeyGet(array('subscriber' =>
288 $this->auth_user->id,
289 'subscribed' => $profile->id));
292 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
297 $notice = $profile->getCurrentNotice();
300 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
304 // StatusNet-specific
306 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
308 return $twitter_user;
311 function twitterStatusArray($notice, $include_user=true)
313 $base = $this->twitterSimpleStatusArray($notice, $include_user);
315 if (!empty($notice->repeat_of)) {
316 $original = Notice::staticGet('id', $notice->repeat_of);
317 if (!empty($original)) {
318 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
319 $base['retweeted_status'] = $original_array;
326 function twitterSimpleStatusArray($notice, $include_user=true)
328 $profile = $notice->getProfile();
330 $twitter_status = array();
331 $twitter_status['text'] = $notice->content;
332 $twitter_status['truncated'] = false; # Not possible on StatusNet
333 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
334 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
335 intval($notice->reply_to) : null;
339 $ns = $notice->getSource();
341 if (!empty($ns->name) && !empty($ns->url)) {
342 $source = '<a href="'
343 . htmlspecialchars($ns->url)
344 . '" rel="nofollow">'
345 . htmlspecialchars($ns->name)
352 $twitter_status['source'] = $source;
353 $twitter_status['id'] = intval($notice->id);
355 $replier_profile = null;
357 if ($notice->reply_to) {
358 $reply = Notice::staticGet(intval($notice->reply_to));
360 $replier_profile = $reply->getProfile();
364 $twitter_status['in_reply_to_user_id'] =
365 ($replier_profile) ? intval($replier_profile->id) : null;
366 $twitter_status['in_reply_to_screen_name'] =
367 ($replier_profile) ? $replier_profile->nickname : null;
369 if (isset($notice->lat) && isset($notice->lon)) {
370 // This is the format that GeoJSON expects stuff to be in
371 $twitter_status['geo'] = array('type' => 'Point',
372 'coordinates' => array((float) $notice->lat,
373 (float) $notice->lon));
375 $twitter_status['geo'] = null;
378 if (isset($this->auth_user)) {
379 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
381 $twitter_status['favorited'] = false;
385 $attachments = $notice->attachments();
387 if (!empty($attachments)) {
389 $twitter_status['attachments'] = array();
391 foreach ($attachments as $attachment) {
392 $enclosure_o=$attachment->getEnclosure();
394 $enclosure = array();
395 $enclosure['url'] = $enclosure_o->url;
396 $enclosure['mimetype'] = $enclosure_o->mimetype;
397 $enclosure['size'] = $enclosure_o->size;
398 $twitter_status['attachments'][] = $enclosure;
403 if ($include_user && $profile) {
404 # Don't get notice (recursive!)
405 $twitter_user = $this->twitterUserArray($profile, false);
406 $twitter_status['user'] = $twitter_user;
409 // StatusNet-specific
411 $twitter_status['statusnet_html'] = $notice->rendered;
413 return $twitter_status;
416 function twitterGroupArray($group)
418 $twitter_group = array();
420 $twitter_group['id'] = $group->id;
421 $twitter_group['url'] = $group->permalink();
422 $twitter_group['nickname'] = $group->nickname;
423 $twitter_group['fullname'] = $group->fullname;
425 if (isset($this->auth_user)) {
426 $twitter_group['member'] = $this->auth_user->isMember($group);
427 $twitter_group['blocked'] = Group_block::isBlocked(
429 $this->auth_user->getProfile()
433 $twitter_group['member_count'] = $group->getMemberCount();
434 $twitter_group['original_logo'] = $group->original_logo;
435 $twitter_group['homepage_logo'] = $group->homepage_logo;
436 $twitter_group['stream_logo'] = $group->stream_logo;
437 $twitter_group['mini_logo'] = $group->mini_logo;
438 $twitter_group['homepage'] = $group->homepage;
439 $twitter_group['description'] = $group->description;
440 $twitter_group['location'] = $group->location;
441 $twitter_group['created'] = $this->dateTwitter($group->created);
442 $twitter_group['modified'] = $this->dateTwitter($group->modified);
444 return $twitter_group;
447 function twitterRssGroupArray($group)
450 $entry['content']=$group->description;
451 $entry['title']=$group->nickname;
452 $entry['link']=$group->permalink();
453 $entry['published']=common_date_iso8601($group->created);
454 $entry['updated']==common_date_iso8601($group->modified);
455 $taguribase = common_config('integration', 'groupuri');
456 $entry['id'] = "group:$groupuribase:$entry[link]";
458 $entry['description'] = $entry['content'];
459 $entry['pubDate'] = common_date_rfc2822($group->created);
460 $entry['guid'] = $entry['link'];
465 function twitterRssEntryArray($notice)
469 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
471 $profile = $notice->getProfile();
473 // We trim() to avoid extraneous whitespace in the output
475 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
476 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
477 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
478 $entry['published'] = common_date_iso8601($notice->created);
480 $taguribase = TagURI::base();
481 $entry['id'] = "tag:$taguribase:$entry[link]";
483 $entry['updated'] = $entry['published'];
484 $entry['author'] = $profile->getBestName();
487 $attachments = $notice->attachments();
488 $enclosures = array();
490 foreach ($attachments as $attachment) {
491 $enclosure_o=$attachment->getEnclosure();
493 $enclosure = array();
494 $enclosure['url'] = $enclosure_o->url;
495 $enclosure['mimetype'] = $enclosure_o->mimetype;
496 $enclosure['size'] = $enclosure_o->size;
497 $enclosures[] = $enclosure;
501 if (!empty($enclosures)) {
502 $entry['enclosures'] = $enclosures;
506 $tag = new Notice_tag();
507 $tag->notice_id = $notice->id;
509 $entry['tags']=array();
510 while ($tag->fetch()) {
511 $entry['tags'][]=$tag->tag;
517 $entry['description'] = $entry['content'];
518 $entry['pubDate'] = common_date_rfc2822($notice->created);
519 $entry['guid'] = $entry['link'];
521 if (isset($notice->lat) && isset($notice->lon)) {
522 // This is the format that GeoJSON expects stuff to be in.
523 // showGeoRSS() below uses it for XML output, so we reuse it
524 $entry['geo'] = array('type' => 'Point',
525 'coordinates' => array((float) $notice->lat,
526 (float) $notice->lon));
528 $entry['geo'] = null;
531 Event::handle('EndRssEntryArray', array($notice, &$entry));
537 function twitterRelationshipArray($source, $target)
539 $relationship = array();
541 $relationship['source'] =
542 $this->relationshipDetailsArray($source, $target);
543 $relationship['target'] =
544 $this->relationshipDetailsArray($target, $source);
546 return array('relationship' => $relationship);
549 function relationshipDetailsArray($source, $target)
553 $details['screen_name'] = $source->nickname;
554 $details['followed_by'] = $target->isSubscribed($source);
555 $details['following'] = $source->isSubscribed($target);
557 $notifications = false;
559 if ($source->isSubscribed($target)) {
561 $sub = Subscription::pkeyGet(array('subscriber' =>
562 $source->id, 'subscribed' => $target->id));
565 $notifications = ($sub->jabber || $sub->sms);
569 $details['notifications_enabled'] = $notifications;
570 $details['blocking'] = $source->hasBlocked($target);
571 $details['id'] = $source->id;
576 function showTwitterXmlRelationship($relationship)
578 $this->elementStart('relationship');
580 foreach($relationship as $element => $value) {
581 if ($element == 'source' || $element == 'target') {
582 $this->elementStart($element);
583 $this->showXmlRelationshipDetails($value);
584 $this->elementEnd($element);
588 $this->elementEnd('relationship');
591 function showXmlRelationshipDetails($details)
593 foreach($details as $element => $value) {
594 $this->element($element, null, $value);
598 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
602 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
604 $this->elementStart($tag, $attrs);
605 foreach($twitter_status as $element => $value) {
608 $this->showTwitterXmlUser($twitter_status['user']);
611 $this->element($element, null, common_xml_safe_str($value));
614 $this->showXmlAttachments($twitter_status['attachments']);
617 $this->showGeoXML($value);
619 case 'retweeted_status':
620 $this->showTwitterXmlStatus($value, 'retweeted_status');
623 if (strncmp($element, 'statusnet_', 10) == 0) {
624 $this->element('statusnet:'.substr($element, 10), null, $value);
626 $this->element($element, null, $value);
630 $this->elementEnd($tag);
633 function showTwitterXmlGroup($twitter_group)
635 $this->elementStart('group');
636 foreach($twitter_group as $element => $value) {
637 $this->element($element, null, $value);
639 $this->elementEnd('group');
642 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
646 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
648 $this->elementStart($role, $attrs);
649 foreach($twitter_user as $element => $value) {
650 if ($element == 'status') {
651 $this->showTwitterXmlStatus($twitter_user['status']);
652 } else if (strncmp($element, 'statusnet_', 10) == 0) {
653 $this->element('statusnet:'.substr($element, 10), null, $value);
655 $this->element($element, null, $value);
658 $this->elementEnd($role);
661 function showXmlAttachments($attachments) {
662 if (!empty($attachments)) {
663 $this->elementStart('attachments', array('type' => 'array'));
664 foreach ($attachments as $attachment) {
666 $attrs['url'] = $attachment['url'];
667 $attrs['mimetype'] = $attachment['mimetype'];
668 $attrs['size'] = $attachment['size'];
669 $this->element('enclosure', $attrs, '');
671 $this->elementEnd('attachments');
675 function showGeoXML($geo)
679 $this->element('geo');
681 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
682 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
683 $this->elementEnd('geo');
687 function showGeoRSS($geo)
693 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
698 function showTwitterRssItem($entry)
700 $this->elementStart('item');
701 $this->element('title', null, $entry['title']);
702 $this->element('description', null, $entry['description']);
703 $this->element('pubDate', null, $entry['pubDate']);
704 $this->element('guid', null, $entry['guid']);
705 $this->element('link', null, $entry['link']);
707 # RSS only supports 1 enclosure per item
708 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
709 $enclosure = $entry['enclosures'][0];
710 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
713 if(array_key_exists('tags', $entry)){
714 foreach($entry['tags'] as $tag){
715 $this->element('category', null,$tag);
719 $this->showGeoRSS($entry['geo']);
720 $this->elementEnd('item');
723 function showJsonObjects($objects)
725 print(json_encode($objects));
728 function showSingleXmlStatus($notice)
730 $this->initDocument('xml');
731 $twitter_status = $this->twitterStatusArray($notice);
732 $this->showTwitterXmlStatus($twitter_status, 'status', true);
733 $this->endDocument('xml');
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)
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)
772 $this->initDocument('rss');
774 $this->element('title', null, $title);
775 $this->element('link', null, $link);
777 if (!is_null($self)) {
781 'type' => 'application/rss+xml',
788 if (!is_null($suplink)) {
789 // For FriendFeed's SUP protocol
790 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
791 'rel' => 'http://api.friendfeed.com/2008/03#sup',
793 'type' => 'application/json'));
796 if (!is_null($logo)) {
797 $this->elementStart('image');
798 $this->element('link', null, $link);
799 $this->element('title', null, $title);
800 $this->element('url', null, $logo);
801 $this->elementEnd('image');
804 $this->element('description', null, $subtitle);
805 $this->element('language', null, 'en-us');
806 $this->element('ttl', null, '40');
808 if (is_array($notice)) {
809 $notice = new ArrayWrapper($notice);
812 while ($notice->fetch()) {
814 $entry = $this->twitterRssEntryArray($notice);
815 $this->showTwitterRssItem($entry);
816 } catch (Exception $e) {
817 common_log(LOG_ERR, $e->getMessage());
818 // continue on exceptions
822 $this->endTwitterRss();
825 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
828 $this->initDocument('atom');
830 $this->element('title', null, $title);
831 $this->element('id', null, $id);
832 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
834 if (!is_null($logo)) {
835 $this->element('logo',null,$logo);
838 if (!is_null($suplink)) {
839 # For FriendFeed's SUP protocol
840 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
842 'type' => 'application/json'));
845 if (!is_null($selfuri)) {
846 $this->element('link', array('href' => $selfuri,
847 'rel' => 'self', 'type' => 'application/atom+xml'), null);
850 $this->element('updated', null, common_date_iso8601('now'));
851 $this->element('subtitle', null, $subtitle);
853 if (is_array($notice)) {
854 $notice = new ArrayWrapper($notice);
857 while ($notice->fetch()) {
859 $this->raw($notice->asAtomEntry());
860 } catch (Exception $e) {
861 common_log(LOG_ERR, $e->getMessage());
866 $this->endDocument('atom');
870 function showRssGroups($group, $title, $link, $subtitle)
873 $this->initDocument('rss');
875 $this->element('title', null, $title);
876 $this->element('link', null, $link);
877 $this->element('description', null, $subtitle);
878 $this->element('language', null, 'en-us');
879 $this->element('ttl', null, '40');
881 if (is_array($group)) {
882 foreach ($group as $g) {
883 $twitter_group = $this->twitterRssGroupArray($g);
884 $this->showTwitterRssItem($twitter_group);
887 while ($group->fetch()) {
888 $twitter_group = $this->twitterRssGroupArray($group);
889 $this->showTwitterRssItem($twitter_group);
893 $this->endTwitterRss();
896 function showTwitterAtomEntry($entry)
898 $this->elementStart('entry');
899 $this->element('title', null, common_xml_safe_str($entry['title']));
902 array('type' => 'html'),
903 common_xml_safe_str($entry['content'])
905 $this->element('id', null, $entry['id']);
906 $this->element('published', null, $entry['published']);
907 $this->element('updated', null, $entry['updated']);
908 $this->element('link', array('type' => 'text/html',
909 'href' => $entry['link'],
910 'rel' => 'alternate'));
911 $this->element('link', array('type' => $entry['avatar-type'],
912 'href' => $entry['avatar'],
914 $this->elementStart('author');
916 $this->element('name', null, $entry['author-name']);
917 $this->element('uri', null, $entry['author-uri']);
919 $this->elementEnd('author');
920 $this->elementEnd('entry');
923 function showXmlDirectMessage($dm, $namespaces=false)
927 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
929 $this->elementStart('direct_message', $attrs);
930 foreach($dm as $element => $value) {
934 $this->showTwitterXmlUser($value, $element);
937 $this->element($element, null, common_xml_safe_str($value));
940 $this->element($element, null, $value);
944 $this->elementEnd('direct_message');
947 function directMessageArray($message)
951 $from_profile = $message->getFrom();
952 $to_profile = $message->getTo();
954 $dmsg['id'] = $message->id;
955 $dmsg['sender_id'] = $message->from_profile;
956 $dmsg['text'] = trim($message->content);
957 $dmsg['recipient_id'] = $message->to_profile;
958 $dmsg['created_at'] = $this->dateTwitter($message->created);
959 $dmsg['sender_screen_name'] = $from_profile->nickname;
960 $dmsg['recipient_screen_name'] = $to_profile->nickname;
961 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
962 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
967 function rssDirectMessageArray($message)
971 $from = $message->getFrom();
973 $entry['title'] = sprintf('Message from %1$s to %2$s',
974 $from->nickname, $message->getTo()->nickname);
976 $entry['content'] = common_xml_safe_str($message->rendered);
977 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
978 $entry['published'] = common_date_iso8601($message->created);
980 $taguribase = TagURI::base();
982 $entry['id'] = "tag:$taguribase:$entry[link]";
983 $entry['updated'] = $entry['published'];
985 $entry['author-name'] = $from->getBestName();
986 $entry['author-uri'] = $from->homepage;
988 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
990 $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
991 $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
995 $entry['description'] = $entry['content'];
996 $entry['pubDate'] = common_date_rfc2822($message->created);
997 $entry['guid'] = $entry['link'];
1002 function showSingleXmlDirectMessage($message)
1004 $this->initDocument('xml');
1005 $dmsg = $this->directMessageArray($message);
1006 $this->showXmlDirectMessage($dmsg, true);
1007 $this->endDocument('xml');
1010 function showSingleJsonDirectMessage($message)
1012 $this->initDocument('json');
1013 $dmsg = $this->directMessageArray($message);
1014 $this->showJsonObjects($dmsg);
1015 $this->endDocument('json');
1018 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1021 $this->initDocument('atom');
1023 $this->element('title', null, common_xml_safe_str($title));
1024 $this->element('id', null, $id);
1025 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1027 if (!is_null($selfuri)) {
1028 $this->element('link', array('href' => $selfuri,
1029 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1032 $this->element('updated', null, common_date_iso8601('now'));
1033 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1035 if (is_array($group)) {
1036 foreach ($group as $g) {
1037 $this->raw($g->asAtomEntry());
1040 while ($group->fetch()) {
1041 $this->raw($group->asAtomEntry());
1045 $this->endDocument('atom');
1049 function showJsonTimeline($notice)
1052 $this->initDocument('json');
1054 $statuses = array();
1056 if (is_array($notice)) {
1057 $notice = new ArrayWrapper($notice);
1060 while ($notice->fetch()) {
1062 $twitter_status = $this->twitterStatusArray($notice);
1063 array_push($statuses, $twitter_status);
1064 } catch (Exception $e) {
1065 common_log(LOG_ERR, $e->getMessage());
1070 $this->showJsonObjects($statuses);
1072 $this->endDocument('json');
1075 function showJsonGroups($group)
1078 $this->initDocument('json');
1082 if (is_array($group)) {
1083 foreach ($group as $g) {
1084 $twitter_group = $this->twitterGroupArray($g);
1085 array_push($groups, $twitter_group);
1088 while ($group->fetch()) {
1089 $twitter_group = $this->twitterGroupArray($group);
1090 array_push($groups, $twitter_group);
1094 $this->showJsonObjects($groups);
1096 $this->endDocument('json');
1099 function showXmlGroups($group)
1102 $this->initDocument('xml');
1103 $this->elementStart('groups', array('type' => 'array'));
1105 if (is_array($group)) {
1106 foreach ($group as $g) {
1107 $twitter_group = $this->twitterGroupArray($g);
1108 $this->showTwitterXmlGroup($twitter_group);
1111 while ($group->fetch()) {
1112 $twitter_group = $this->twitterGroupArray($group);
1113 $this->showTwitterXmlGroup($twitter_group);
1117 $this->elementEnd('groups');
1118 $this->endDocument('xml');
1121 function showTwitterXmlUsers($user)
1124 $this->initDocument('xml');
1125 $this->elementStart('users', array('type' => 'array',
1126 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1128 if (is_array($user)) {
1129 foreach ($user as $u) {
1130 $twitter_user = $this->twitterUserArray($u);
1131 $this->showTwitterXmlUser($twitter_user);
1134 while ($user->fetch()) {
1135 $twitter_user = $this->twitterUserArray($user);
1136 $this->showTwitterXmlUser($twitter_user);
1140 $this->elementEnd('users');
1141 $this->endDocument('xml');
1144 function showJsonUsers($user)
1147 $this->initDocument('json');
1151 if (is_array($user)) {
1152 foreach ($user as $u) {
1153 $twitter_user = $this->twitterUserArray($u);
1154 array_push($users, $twitter_user);
1157 while ($user->fetch()) {
1158 $twitter_user = $this->twitterUserArray($user);
1159 array_push($users, $twitter_user);
1163 $this->showJsonObjects($users);
1165 $this->endDocument('json');
1168 function showSingleJsonGroup($group)
1170 $this->initDocument('json');
1171 $twitter_group = $this->twitterGroupArray($group);
1172 $this->showJsonObjects($twitter_group);
1173 $this->endDocument('json');
1176 function showSingleXmlGroup($group)
1178 $this->initDocument('xml');
1179 $twitter_group = $this->twitterGroupArray($group);
1180 $this->showTwitterXmlGroup($twitter_group);
1181 $this->endDocument('xml');
1184 function dateTwitter($dt)
1186 $dateStr = date('d F Y H:i:s', strtotime($dt));
1187 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1188 $d->setTimezone(new DateTimeZone(common_timezone()));
1189 return $d->format('D M d H:i:s O Y');
1192 function initDocument($type='xml')
1196 header('Content-Type: application/xml; charset=utf-8');
1200 header('Content-Type: application/json; charset=utf-8');
1202 // Check for JSONP callback
1203 if (isset($this->callback)) {
1204 print $this->callback . '(';
1208 header("Content-Type: application/rss+xml; charset=utf-8");
1209 $this->initTwitterRss();
1212 header('Content-Type: application/atom+xml; charset=utf-8');
1213 $this->initTwitterAtom();
1216 // TRANS: Client error on an API request with an unsupported data format.
1217 $this->clientError(_('Not a supported data format.'));
1224 function endDocument($type='xml')
1232 // Check for JSONP callback
1233 if (isset($this->callback)) {
1238 $this->endTwitterRss();
1241 $this->endTwitterRss();
1244 // TRANS: Client error on an API request with an unsupported data format.
1245 $this->clientError(_('Not a supported data format.'));
1251 function clientError($msg, $code = 400, $format = 'xml')
1253 $action = $this->trimmed('action');
1255 common_debug("User error '$code' on '$action': $msg", __FILE__);
1257 if (!array_key_exists($code, ClientErrorAction::$status)) {
1261 $status_string = ClientErrorAction::$status[$code];
1263 // Do not emit error header for JSONP
1264 if (!isset($this->callback)) {
1265 header('HTTP/1.1 '.$code.' '.$status_string);
1268 if ($format == 'xml') {
1269 $this->initDocument('xml');
1270 $this->elementStart('hash');
1271 $this->element('error', null, $msg);
1272 $this->element('request', null, $_SERVER['REQUEST_URI']);
1273 $this->elementEnd('hash');
1274 $this->endDocument('xml');
1275 } elseif ($format == 'json'){
1276 $this->initDocument('json');
1277 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1278 print(json_encode($error_array));
1279 $this->endDocument('json');
1282 // If user didn't request a useful format, throw a regular client error
1283 throw new ClientException($msg, $code);
1287 function serverError($msg, $code = 500, $content_type = 'xml')
1289 $action = $this->trimmed('action');
1291 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1293 if (!array_key_exists($code, ServerErrorAction::$status)) {
1297 $status_string = ServerErrorAction::$status[$code];
1299 // Do not emit error header for JSONP
1300 if (!isset($this->callback)) {
1301 header('HTTP/1.1 '.$code.' '.$status_string);
1304 if ($content_type == 'xml') {
1305 $this->initDocument('xml');
1306 $this->elementStart('hash');
1307 $this->element('error', null, $msg);
1308 $this->element('request', null, $_SERVER['REQUEST_URI']);
1309 $this->elementEnd('hash');
1310 $this->endDocument('xml');
1312 $this->initDocument('json');
1313 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1314 print(json_encode($error_array));
1315 $this->endDocument('json');
1319 function initTwitterRss()
1322 $this->elementStart(
1326 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1327 'xmlns:georss' => 'http://www.georss.org/georss'
1330 $this->elementStart('channel');
1331 Event::handle('StartApiRss', array($this));
1334 function endTwitterRss()
1336 $this->elementEnd('channel');
1337 $this->elementEnd('rss');
1341 function initTwitterAtom()
1344 // FIXME: don't hardcode the language here!
1345 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1346 'xml:lang' => 'en-US',
1347 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1350 function endTwitterAtom()
1352 $this->elementEnd('feed');
1356 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1358 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1359 switch ($content_type) {
1361 $this->showTwitterXmlUser($profile_array);
1364 $this->showJsonObjects($profile_array);
1367 // TRANS: Client error on an API request with an unsupported data format.
1368 $this->clientError(_('Not a supported data format.'));
1374 function getTargetUser($id)
1378 // Twitter supports these other ways of passing the user ID
1379 if (is_numeric($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 (is_numeric($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 (is_numeric($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 (is_numeric($this->arg('id'))) {
1412 return Profile::staticGet($this->arg('id'));
1413 } else if ($this->arg('id')) {
1414 $nickname = common_canonical_nickname($this->arg('id'));
1415 return Profile::staticGet('nickname', $nickname);
1416 } else if ($this->arg('user_id')) {
1417 // This is to ensure that a non-numeric user_id still
1418 // overrides screen_name even if it doesn't get used
1419 if (is_numeric($this->arg('user_id'))) {
1420 return Profile::staticGet('id', $this->arg('user_id'));
1422 } else if ($this->arg('screen_name')) {
1423 $nickname = common_canonical_nickname($this->arg('screen_name'));
1424 return Profile::staticGet('nickname', $nickname);
1426 } else if (is_numeric($id)) {
1427 return Profile::staticGet($id);
1429 $nickname = common_canonical_nickname($id);
1430 return Profile::staticGet('nickname', $nickname);
1434 function getTargetGroup($id)
1437 if (is_numeric($this->arg('id'))) {
1438 return User_group::staticGet($this->arg('id'));
1439 } else if ($this->arg('id')) {
1440 $nickname = common_canonical_nickname($this->arg('id'));
1441 $local = Local_group::staticGet('nickname', $nickname);
1442 if (empty($local)) {
1445 return User_group::staticGet('id', $local->id);
1447 } else if ($this->arg('group_id')) {
1448 // This is to ensure that a non-numeric user_id still
1449 // overrides screen_name even if it doesn't get used
1450 if (is_numeric($this->arg('group_id'))) {
1451 return User_group::staticGet('id', $this->arg('group_id'));
1453 } else if ($this->arg('group_name')) {
1454 $nickname = common_canonical_nickname($this->arg('group_name'));
1455 $local = Local_group::staticGet('nickname', $nickname);
1456 if (empty($local)) {
1459 return User_group::staticGet('id', $local->group_id);
1463 } else if (is_numeric($id)) {
1464 return User_group::staticGet($id);
1466 $nickname = common_canonical_nickname($id);
1467 $local = Local_group::staticGet('nickname', $nickname);
1468 if (empty($local)) {
1471 return User_group::staticGet('id', $local->group_id);
1477 * Returns query argument or default value if not found. Certain
1478 * parameters used throughout the API are lightly scrubbed and
1479 * bounds checked. This overrides Action::arg().
1481 * @param string $key requested argument
1482 * @param string $def default value to return if $key is not provided
1486 function arg($key, $def=null)
1489 // XXX: Do even more input validation/scrubbing?
1491 if (array_key_exists($key, $this->args)) {
1494 $page = (int)$this->args['page'];
1495 return ($page < 1) ? 1 : $page;
1497 $count = (int)$this->args['count'];
1500 } elseif ($count > 200) {
1506 $since_id = (int)$this->args['since_id'];
1507 return ($since_id < 1) ? 0 : $since_id;
1509 $max_id = (int)$this->args['max_id'];
1510 return ($max_id < 1) ? 0 : $max_id;
1512 return parent::arg($key, $def);
1520 * Calculate the complete URI that called up this action. Used for
1521 * Atom rel="self" links. Warning: this is funky.
1523 * @return string URL a URL suitable for rel="self" Atom links
1525 function getSelfUri()
1527 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1529 $id = $this->arg('id');
1530 $aargs = array('format' => $this->format);
1535 $tag = $this->arg('tag');
1537 $aargs['tag'] = $tag;
1540 parse_str($_SERVER['QUERY_STRING'], $params);
1542 if (!empty($params)) {
1543 unset($params['p']);
1544 $pstring = http_build_query($params);
1547 $uri = common_local_url($action, $aargs);
1549 if (!empty($pstring)) {
1550 $uri .= '?' . $pstring;