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();
206 $user = $profile->getUser();
207 } catch (NoSuchUserException $e) {
211 $twitter_user['id'] = intval($profile->id);
212 $twitter_user['name'] = $profile->getBestName();
213 $twitter_user['screen_name'] = $profile->nickname;
214 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
215 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
217 $twitter_user['profile_image_url'] = $profile->avatarUrl(AVATAR_STREAM_SIZE);
219 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
220 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
221 $twitter_user['followers_count'] = $profile->subscriberCount();
223 // Note: some profiles don't have an associated user
225 $twitter_user['friends_count'] = $profile->subscriptionCount();
227 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
229 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
233 if (!empty($user) && $user->timezone) {
234 $timezone = $user->timezone;
238 $t->setTimezone(new DateTimeZone($timezone));
240 $twitter_user['utc_offset'] = $t->format('Z');
241 $twitter_user['time_zone'] = $timezone;
242 $twitter_user['statuses_count'] = $profile->noticeCount();
244 // Is the requesting user following this user?
245 $twitter_user['following'] = false;
246 $twitter_user['statusnet_blocking'] = false;
247 $twitter_user['notifications'] = false;
249 if (isset($this->auth_user)) {
251 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
252 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
255 $sub = Subscription::pkeyGet(array('subscriber' =>
256 $this->auth_user->id,
257 'subscribed' => $profile->id));
260 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
265 $notice = $profile->getCurrentNotice();
268 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
272 // StatusNet-specific
274 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
276 return $twitter_user;
279 function twitterStatusArray($notice, $include_user=true)
281 $base = $this->twitterSimpleStatusArray($notice, $include_user);
283 if (!empty($notice->repeat_of)) {
284 $original = Notice::getKV('id', $notice->repeat_of);
285 if (!empty($original)) {
286 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
287 $base['retweeted_status'] = $original_array;
294 function twitterSimpleStatusArray($notice, $include_user=true)
296 $profile = $notice->getProfile();
298 $twitter_status = array();
299 $twitter_status['text'] = $notice->content;
300 $twitter_status['truncated'] = false; # Not possible on StatusNet
301 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
302 $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
303 intval($notice->reply_to) : null;
307 $ns = $notice->getSource();
309 if (!empty($ns->name) && !empty($ns->url)) {
310 $source = '<a href="'
311 . htmlspecialchars($ns->url)
312 . '" rel="nofollow">'
313 . htmlspecialchars($ns->name)
320 $twitter_status['source'] = $source;
321 $twitter_status['id'] = intval($notice->id);
323 $replier_profile = null;
325 if ($notice->reply_to) {
326 $reply = Notice::getKV(intval($notice->reply_to));
328 $replier_profile = $reply->getProfile();
332 $twitter_status['in_reply_to_user_id'] =
333 ($replier_profile) ? intval($replier_profile->id) : null;
334 $twitter_status['in_reply_to_screen_name'] =
335 ($replier_profile) ? $replier_profile->nickname : null;
337 if (isset($notice->lat) && isset($notice->lon)) {
338 // This is the format that GeoJSON expects stuff to be in
339 $twitter_status['geo'] = array('type' => 'Point',
340 'coordinates' => array((float) $notice->lat,
341 (float) $notice->lon));
343 $twitter_status['geo'] = null;
346 if (isset($this->auth_user)) {
347 $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
349 $twitter_status['favorited'] = false;
353 $attachments = $notice->attachments();
355 if (!empty($attachments)) {
357 $twitter_status['attachments'] = array();
359 foreach ($attachments as $attachment) {
360 $enclosure_o=$attachment->getEnclosure();
362 $enclosure = array();
363 $enclosure['url'] = $enclosure_o->url;
364 $enclosure['mimetype'] = $enclosure_o->mimetype;
365 $enclosure['size'] = $enclosure_o->size;
366 $twitter_status['attachments'][] = $enclosure;
371 if ($include_user && $profile) {
372 // Don't get notice (recursive!)
373 $twitter_user = $this->twitterUserArray($profile, false);
374 $twitter_status['user'] = $twitter_user;
377 // StatusNet-specific
379 $twitter_status['statusnet_html'] = $notice->rendered;
380 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
382 return $twitter_status;
385 function twitterGroupArray($group)
387 $twitter_group = array();
389 $twitter_group['id'] = intval($group->id);
390 $twitter_group['url'] = $group->permalink();
391 $twitter_group['nickname'] = $group->nickname;
392 $twitter_group['fullname'] = $group->fullname;
394 if (isset($this->auth_user)) {
395 $twitter_group['member'] = $this->auth_user->isMember($group);
396 $twitter_group['blocked'] = Group_block::isBlocked(
398 $this->auth_user->getProfile()
402 $twitter_group['member_count'] = $group->getMemberCount();
403 $twitter_group['original_logo'] = $group->original_logo;
404 $twitter_group['homepage_logo'] = $group->homepage_logo;
405 $twitter_group['stream_logo'] = $group->stream_logo;
406 $twitter_group['mini_logo'] = $group->mini_logo;
407 $twitter_group['homepage'] = $group->homepage;
408 $twitter_group['description'] = $group->description;
409 $twitter_group['location'] = $group->location;
410 $twitter_group['created'] = $this->dateTwitter($group->created);
411 $twitter_group['modified'] = $this->dateTwitter($group->modified);
413 return $twitter_group;
416 function twitterRssGroupArray($group)
419 $entry['content']=$group->description;
420 $entry['title']=$group->nickname;
421 $entry['link']=$group->permalink();
422 $entry['published']=common_date_iso8601($group->created);
423 $entry['updated']==common_date_iso8601($group->modified);
424 $taguribase = common_config('integration', 'groupuri');
425 $entry['id'] = "group:$groupuribase:$entry[link]";
427 $entry['description'] = $entry['content'];
428 $entry['pubDate'] = common_date_rfc2822($group->created);
429 $entry['guid'] = $entry['link'];
434 function twitterListArray($list)
436 $profile = Profile::getKV('id', $list->tagger);
438 $twitter_list = array();
439 $twitter_list['id'] = $list->id;
440 $twitter_list['name'] = $list->tag;
441 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
442 $twitter_list['slug'] = $list->tag;
443 $twitter_list['description'] = $list->description;
444 $twitter_list['subscriber_count'] = $list->subscriberCount();
445 $twitter_list['member_count'] = $list->taggedCount();
446 $twitter_list['uri'] = $list->getUri();
448 if (isset($this->auth_user)) {
449 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
451 $twitter_list['following'] = false;
454 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
455 $twitter_list['user'] = $this->twitterUserArray($profile, false);
457 return $twitter_list;
460 function twitterRssEntryArray($notice)
464 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
465 $profile = $notice->getProfile();
467 // We trim() to avoid extraneous whitespace in the output
469 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
470 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
471 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
472 $entry['published'] = common_date_iso8601($notice->created);
474 $taguribase = TagURI::base();
475 $entry['id'] = "tag:$taguribase:$entry[link]";
477 $entry['updated'] = $entry['published'];
478 $entry['author'] = $profile->getBestName();
481 $attachments = $notice->attachments();
482 $enclosures = array();
484 foreach ($attachments as $attachment) {
485 $enclosure_o=$attachment->getEnclosure();
487 $enclosure = array();
488 $enclosure['url'] = $enclosure_o->url;
489 $enclosure['mimetype'] = $enclosure_o->mimetype;
490 $enclosure['size'] = $enclosure_o->size;
491 $enclosures[] = $enclosure;
495 if (!empty($enclosures)) {
496 $entry['enclosures'] = $enclosures;
500 $tag = new Notice_tag();
501 $tag->notice_id = $notice->id;
503 $entry['tags']=array();
504 while ($tag->fetch()) {
505 $entry['tags'][]=$tag->tag;
511 $entry['description'] = $entry['content'];
512 $entry['pubDate'] = common_date_rfc2822($notice->created);
513 $entry['guid'] = $entry['link'];
515 if (isset($notice->lat) && isset($notice->lon)) {
516 // This is the format that GeoJSON expects stuff to be in.
517 // showGeoRSS() below uses it for XML output, so we reuse it
518 $entry['geo'] = array('type' => 'Point',
519 'coordinates' => array((float) $notice->lat,
520 (float) $notice->lon));
522 $entry['geo'] = null;
525 Event::handle('EndRssEntryArray', array($notice, &$entry));
531 function twitterRelationshipArray($source, $target)
533 $relationship = array();
535 $relationship['source'] =
536 $this->relationshipDetailsArray($source, $target);
537 $relationship['target'] =
538 $this->relationshipDetailsArray($target, $source);
540 return array('relationship' => $relationship);
543 function relationshipDetailsArray($source, $target)
547 $details['screen_name'] = $source->nickname;
548 $details['followed_by'] = $target->isSubscribed($source);
549 $details['following'] = $source->isSubscribed($target);
551 $notifications = false;
553 if ($source->isSubscribed($target)) {
554 $sub = Subscription::pkeyGet(array('subscriber' =>
555 $source->id, 'subscribed' => $target->id));
558 $notifications = ($sub->jabber || $sub->sms);
562 $details['notifications_enabled'] = $notifications;
563 $details['blocking'] = $source->hasBlocked($target);
564 $details['id'] = intval($source->id);
569 function showTwitterXmlRelationship($relationship)
571 $this->elementStart('relationship');
573 foreach($relationship as $element => $value) {
574 if ($element == 'source' || $element == 'target') {
575 $this->elementStart($element);
576 $this->showXmlRelationshipDetails($value);
577 $this->elementEnd($element);
581 $this->elementEnd('relationship');
584 function showXmlRelationshipDetails($details)
586 foreach($details as $element => $value) {
587 $this->element($element, null, $value);
591 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
595 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
597 $this->elementStart($tag, $attrs);
598 foreach($twitter_status as $element => $value) {
601 $this->showTwitterXmlUser($twitter_status['user']);
604 $this->element($element, null, common_xml_safe_str($value));
607 $this->showXmlAttachments($twitter_status['attachments']);
610 $this->showGeoXML($value);
612 case 'retweeted_status':
613 $this->showTwitterXmlStatus($value, 'retweeted_status');
616 if (strncmp($element, 'statusnet_', 10) == 0) {
617 $this->element('statusnet:'.substr($element, 10), null, $value);
619 $this->element($element, null, $value);
623 $this->elementEnd($tag);
626 function showTwitterXmlGroup($twitter_group)
628 $this->elementStart('group');
629 foreach($twitter_group as $element => $value) {
630 $this->element($element, null, $value);
632 $this->elementEnd('group');
635 function showTwitterXmlList($twitter_list)
637 $this->elementStart('list');
638 foreach($twitter_list as $element => $value) {
639 if($element == 'user') {
640 $this->showTwitterXmlUser($value, 'user');
643 $this->element($element, null, $value);
646 $this->elementEnd('list');
649 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
653 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
655 $this->elementStart($role, $attrs);
656 foreach($twitter_user as $element => $value) {
657 if ($element == 'status') {
658 $this->showTwitterXmlStatus($twitter_user['status']);
659 } else if (strncmp($element, 'statusnet_', 10) == 0) {
660 $this->element('statusnet:'.substr($element, 10), null, $value);
662 $this->element($element, null, $value);
665 $this->elementEnd($role);
668 function showXmlAttachments($attachments) {
669 if (!empty($attachments)) {
670 $this->elementStart('attachments', array('type' => 'array'));
671 foreach ($attachments as $attachment) {
673 $attrs['url'] = $attachment['url'];
674 $attrs['mimetype'] = $attachment['mimetype'];
675 $attrs['size'] = $attachment['size'];
676 $this->element('enclosure', $attrs, '');
678 $this->elementEnd('attachments');
682 function showGeoXML($geo)
686 $this->element('geo');
688 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
689 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
690 $this->elementEnd('geo');
694 function showGeoRSS($geo)
700 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
705 function showTwitterRssItem($entry)
707 $this->elementStart('item');
708 $this->element('title', null, $entry['title']);
709 $this->element('description', null, $entry['description']);
710 $this->element('pubDate', null, $entry['pubDate']);
711 $this->element('guid', null, $entry['guid']);
712 $this->element('link', null, $entry['link']);
714 // RSS only supports 1 enclosure per item
715 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
716 $enclosure = $entry['enclosures'][0];
717 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
720 if(array_key_exists('tags', $entry)){
721 foreach($entry['tags'] as $tag){
722 $this->element('category', null,$tag);
726 $this->showGeoRSS($entry['geo']);
727 $this->elementEnd('item');
730 function showJsonObjects($objects)
732 print(json_encode($objects));
735 function showSingleXmlStatus($notice)
737 $this->initDocument('xml');
738 $twitter_status = $this->twitterStatusArray($notice);
739 $this->showTwitterXmlStatus($twitter_status, 'status', true);
740 $this->endDocument('xml');
743 function showSingleAtomStatus($notice)
745 header('Content-Type: application/atom+xml; charset=utf-8');
746 print $notice->asAtomEntry(true, true, true, $this->auth_user);
749 function show_single_json_status($notice)
751 $this->initDocument('json');
752 $status = $this->twitterStatusArray($notice);
753 $this->showJsonObjects($status);
754 $this->endDocument('json');
757 function showXmlTimeline($notice)
759 $this->initDocument('xml');
760 $this->elementStart('statuses', array('type' => 'array',
761 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
763 if (is_array($notice)) {
764 $notice = new ArrayWrapper($notice);
767 while ($notice->fetch()) {
769 $twitter_status = $this->twitterStatusArray($notice);
770 $this->showTwitterXmlStatus($twitter_status);
771 } catch (Exception $e) {
772 common_log(LOG_ERR, $e->getMessage());
777 $this->elementEnd('statuses');
778 $this->endDocument('xml');
781 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
783 $this->initDocument('rss');
785 $this->element('title', null, $title);
786 $this->element('link', null, $link);
788 if (!is_null($self)) {
792 'type' => 'application/rss+xml',
799 if (!is_null($suplink)) {
800 // For FriendFeed's SUP protocol
801 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
802 'rel' => 'http://api.friendfeed.com/2008/03#sup',
804 'type' => 'application/json'));
807 if (!is_null($logo)) {
808 $this->elementStart('image');
809 $this->element('link', null, $link);
810 $this->element('title', null, $title);
811 $this->element('url', null, $logo);
812 $this->elementEnd('image');
815 $this->element('description', null, $subtitle);
816 $this->element('language', null, 'en-us');
817 $this->element('ttl', null, '40');
819 if (is_array($notice)) {
820 $notice = new ArrayWrapper($notice);
823 while ($notice->fetch()) {
825 $entry = $this->twitterRssEntryArray($notice);
826 $this->showTwitterRssItem($entry);
827 } catch (Exception $e) {
828 common_log(LOG_ERR, $e->getMessage());
829 // continue on exceptions
833 $this->endTwitterRss();
836 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
838 $this->initDocument('atom');
840 $this->element('title', null, $title);
841 $this->element('id', null, $id);
842 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
844 if (!is_null($logo)) {
845 $this->element('logo',null,$logo);
848 if (!is_null($suplink)) {
849 // For FriendFeed's SUP protocol
850 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
852 'type' => 'application/json'));
855 if (!is_null($selfuri)) {
856 $this->element('link', array('href' => $selfuri,
857 'rel' => 'self', 'type' => 'application/atom+xml'), null);
860 $this->element('updated', null, common_date_iso8601('now'));
861 $this->element('subtitle', null, $subtitle);
863 if (is_array($notice)) {
864 $notice = new ArrayWrapper($notice);
867 while ($notice->fetch()) {
869 $this->raw($notice->asAtomEntry());
870 } catch (Exception $e) {
871 common_log(LOG_ERR, $e->getMessage());
876 $this->endDocument('atom');
879 function showRssGroups($group, $title, $link, $subtitle)
881 $this->initDocument('rss');
883 $this->element('title', null, $title);
884 $this->element('link', null, $link);
885 $this->element('description', null, $subtitle);
886 $this->element('language', null, 'en-us');
887 $this->element('ttl', null, '40');
889 if (is_array($group)) {
890 foreach ($group as $g) {
891 $twitter_group = $this->twitterRssGroupArray($g);
892 $this->showTwitterRssItem($twitter_group);
895 while ($group->fetch()) {
896 $twitter_group = $this->twitterRssGroupArray($group);
897 $this->showTwitterRssItem($twitter_group);
901 $this->endTwitterRss();
904 function showTwitterAtomEntry($entry)
906 $this->elementStart('entry');
907 $this->element('title', null, common_xml_safe_str($entry['title']));
910 array('type' => 'html'),
911 common_xml_safe_str($entry['content'])
913 $this->element('id', null, $entry['id']);
914 $this->element('published', null, $entry['published']);
915 $this->element('updated', null, $entry['updated']);
916 $this->element('link', array('type' => 'text/html',
917 'href' => $entry['link'],
918 'rel' => 'alternate'));
919 $this->element('link', array('type' => $entry['avatar-type'],
920 'href' => $entry['avatar'],
922 $this->elementStart('author');
924 $this->element('name', null, $entry['author-name']);
925 $this->element('uri', null, $entry['author-uri']);
927 $this->elementEnd('author');
928 $this->elementEnd('entry');
931 function showXmlDirectMessage($dm, $namespaces=false)
935 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
937 $this->elementStart('direct_message', $attrs);
938 foreach($dm as $element => $value) {
942 $this->showTwitterXmlUser($value, $element);
945 $this->element($element, null, common_xml_safe_str($value));
948 $this->element($element, null, $value);
952 $this->elementEnd('direct_message');
955 function directMessageArray($message)
959 $from_profile = $message->getFrom();
960 $to_profile = $message->getTo();
962 $dmsg['id'] = intval($message->id);
963 $dmsg['sender_id'] = intval($from_profile->id);
964 $dmsg['text'] = trim($message->content);
965 $dmsg['recipient_id'] = intval($to_profile->id);
966 $dmsg['created_at'] = $this->dateTwitter($message->created);
967 $dmsg['sender_screen_name'] = $from_profile->nickname;
968 $dmsg['recipient_screen_name'] = $to_profile->nickname;
969 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
970 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
975 function rssDirectMessageArray($message)
979 $from = $message->getFrom();
981 $entry['title'] = sprintf('Message from %1$s to %2$s',
982 $from->nickname, $message->getTo()->nickname);
984 $entry['content'] = common_xml_safe_str($message->rendered);
985 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
986 $entry['published'] = common_date_iso8601($message->created);
988 $taguribase = TagURI::base();
990 $entry['id'] = "tag:$taguribase:$entry[link]";
991 $entry['updated'] = $entry['published'];
993 $entry['author-name'] = $from->getBestName();
994 $entry['author-uri'] = $from->homepage;
996 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
998 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
999 $entry['avatar-type'] = $avatar->mediatype;
1000 } catch (Exception $e) {
1001 $entry['avatar-type'] = 'image/png';
1004 // RSS item specific
1006 $entry['description'] = $entry['content'];
1007 $entry['pubDate'] = common_date_rfc2822($message->created);
1008 $entry['guid'] = $entry['link'];
1013 function showSingleXmlDirectMessage($message)
1015 $this->initDocument('xml');
1016 $dmsg = $this->directMessageArray($message);
1017 $this->showXmlDirectMessage($dmsg, true);
1018 $this->endDocument('xml');
1021 function showSingleJsonDirectMessage($message)
1023 $this->initDocument('json');
1024 $dmsg = $this->directMessageArray($message);
1025 $this->showJsonObjects($dmsg);
1026 $this->endDocument('json');
1029 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1031 $this->initDocument('atom');
1033 $this->element('title', null, common_xml_safe_str($title));
1034 $this->element('id', null, $id);
1035 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1037 if (!is_null($selfuri)) {
1038 $this->element('link', array('href' => $selfuri,
1039 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1042 $this->element('updated', null, common_date_iso8601('now'));
1043 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1045 if (is_array($group)) {
1046 foreach ($group as $g) {
1047 $this->raw($g->asAtomEntry());
1050 while ($group->fetch()) {
1051 $this->raw($group->asAtomEntry());
1055 $this->endDocument('atom');
1059 function showJsonTimeline($notice)
1061 $this->initDocument('json');
1063 $statuses = array();
1065 if (is_array($notice)) {
1066 $notice = new ArrayWrapper($notice);
1069 while ($notice->fetch()) {
1071 $twitter_status = $this->twitterStatusArray($notice);
1072 array_push($statuses, $twitter_status);
1073 } catch (Exception $e) {
1074 common_log(LOG_ERR, $e->getMessage());
1079 $this->showJsonObjects($statuses);
1081 $this->endDocument('json');
1084 function showJsonGroups($group)
1086 $this->initDocument('json');
1090 if (is_array($group)) {
1091 foreach ($group as $g) {
1092 $twitter_group = $this->twitterGroupArray($g);
1093 array_push($groups, $twitter_group);
1096 while ($group->fetch()) {
1097 $twitter_group = $this->twitterGroupArray($group);
1098 array_push($groups, $twitter_group);
1102 $this->showJsonObjects($groups);
1104 $this->endDocument('json');
1107 function showXmlGroups($group)
1110 $this->initDocument('xml');
1111 $this->elementStart('groups', array('type' => 'array'));
1113 if (is_array($group)) {
1114 foreach ($group as $g) {
1115 $twitter_group = $this->twitterGroupArray($g);
1116 $this->showTwitterXmlGroup($twitter_group);
1119 while ($group->fetch()) {
1120 $twitter_group = $this->twitterGroupArray($group);
1121 $this->showTwitterXmlGroup($twitter_group);
1125 $this->elementEnd('groups');
1126 $this->endDocument('xml');
1129 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1132 $this->initDocument('xml');
1133 $this->elementStart('lists_list');
1134 $this->elementStart('lists', array('type' => 'array'));
1136 if (is_array($list)) {
1137 foreach ($list as $l) {
1138 $twitter_list = $this->twitterListArray($l);
1139 $this->showTwitterXmlList($twitter_list);
1142 while ($list->fetch()) {
1143 $twitter_list = $this->twitterListArray($list);
1144 $this->showTwitterXmlList($twitter_list);
1148 $this->elementEnd('lists');
1150 $this->element('next_cursor', null, $next_cursor);
1151 $this->element('previous_cursor', null, $prev_cursor);
1153 $this->elementEnd('lists_list');
1154 $this->endDocument('xml');
1157 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1159 $this->initDocument('json');
1163 if (is_array($list)) {
1164 foreach ($list as $l) {
1165 $twitter_list = $this->twitterListArray($l);
1166 array_push($lists, $twitter_list);
1169 while ($list->fetch()) {
1170 $twitter_list = $this->twitterListArray($list);
1171 array_push($lists, $twitter_list);
1175 $lists_list = array(
1177 'next_cursor' => $next_cursor,
1178 'next_cursor_str' => strval($next_cursor),
1179 'previous_cursor' => $prev_cursor,
1180 'previous_cursor_str' => strval($prev_cursor)
1183 $this->showJsonObjects($lists_list);
1185 $this->endDocument('json');
1188 function showTwitterXmlUsers($user)
1190 $this->initDocument('xml');
1191 $this->elementStart('users', array('type' => 'array',
1192 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1194 if (is_array($user)) {
1195 foreach ($user as $u) {
1196 $twitter_user = $this->twitterUserArray($u);
1197 $this->showTwitterXmlUser($twitter_user);
1200 while ($user->fetch()) {
1201 $twitter_user = $this->twitterUserArray($user);
1202 $this->showTwitterXmlUser($twitter_user);
1206 $this->elementEnd('users');
1207 $this->endDocument('xml');
1210 function showJsonUsers($user)
1212 $this->initDocument('json');
1216 if (is_array($user)) {
1217 foreach ($user as $u) {
1218 $twitter_user = $this->twitterUserArray($u);
1219 array_push($users, $twitter_user);
1222 while ($user->fetch()) {
1223 $twitter_user = $this->twitterUserArray($user);
1224 array_push($users, $twitter_user);
1228 $this->showJsonObjects($users);
1230 $this->endDocument('json');
1233 function showSingleJsonGroup($group)
1235 $this->initDocument('json');
1236 $twitter_group = $this->twitterGroupArray($group);
1237 $this->showJsonObjects($twitter_group);
1238 $this->endDocument('json');
1241 function showSingleXmlGroup($group)
1243 $this->initDocument('xml');
1244 $twitter_group = $this->twitterGroupArray($group);
1245 $this->showTwitterXmlGroup($twitter_group);
1246 $this->endDocument('xml');
1249 function showSingleJsonList($list)
1251 $this->initDocument('json');
1252 $twitter_list = $this->twitterListArray($list);
1253 $this->showJsonObjects($twitter_list);
1254 $this->endDocument('json');
1257 function showSingleXmlList($list)
1259 $this->initDocument('xml');
1260 $twitter_list = $this->twitterListArray($list);
1261 $this->showTwitterXmlList($twitter_list);
1262 $this->endDocument('xml');
1265 function dateTwitter($dt)
1267 $dateStr = date('d F Y H:i:s', strtotime($dt));
1268 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1269 $d->setTimezone(new DateTimeZone(common_timezone()));
1270 return $d->format('D M d H:i:s O Y');
1273 function initDocument($type='xml')
1277 header('Content-Type: application/xml; charset=utf-8');
1281 header('Content-Type: application/json; charset=utf-8');
1283 // Check for JSONP callback
1284 if (isset($this->callback)) {
1285 print $this->callback . '(';
1289 header("Content-Type: application/rss+xml; charset=utf-8");
1290 $this->initTwitterRss();
1293 header('Content-Type: application/atom+xml; charset=utf-8');
1294 $this->initTwitterAtom();
1297 // TRANS: Client error on an API request with an unsupported data format.
1298 $this->clientError(_('Not a supported data format.'));
1305 function endDocument($type='xml')
1312 // Check for JSONP callback
1313 if (isset($this->callback)) {
1318 $this->endTwitterRss();
1321 $this->endTwitterRss();
1324 // TRANS: Client error on an API request with an unsupported data format.
1325 $this->clientError(_('Not a supported data format.'));
1331 function clientError($msg, $code = 400, $format = null)
1333 $action = $this->trimmed('action');
1334 if ($format === null) {
1335 $format = $this->format;
1338 common_debug("User error '$code' on '$action': $msg", __FILE__);
1340 if (!array_key_exists($code, ClientErrorAction::$status)) {
1344 $status_string = ClientErrorAction::$status[$code];
1346 // Do not emit error header for JSONP
1347 if (!isset($this->callback)) {
1348 header('HTTP/1.1 ' . $code . ' ' . $status_string);
1353 $this->initDocument('xml');
1354 $this->elementStart('hash');
1355 $this->element('error', null, $msg);
1356 $this->element('request', null, $_SERVER['REQUEST_URI']);
1357 $this->elementEnd('hash');
1358 $this->endDocument('xml');
1361 $this->initDocument('json');
1362 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1363 print(json_encode($error_array));
1364 $this->endDocument('json');
1367 header('Content-Type: text/plain; charset=utf-8');
1371 // If user didn't request a useful format, throw a regular client error
1372 throw new ClientException($msg, $code);
1376 function serverError($msg, $code = 500, $content_type = null)
1378 $action = $this->trimmed('action');
1379 if ($content_type === null) {
1380 $content_type = $this->format;
1383 common_debug("Server error '$code' on '$action': $msg", __FILE__);
1385 if (!array_key_exists($code, ServerErrorAction::$status)) {
1389 $status_string = ServerErrorAction::$status[$code];
1391 // Do not emit error header for JSONP
1392 if (!isset($this->callback)) {
1393 header('HTTP/1.1 '.$code.' '.$status_string);
1396 if ($content_type == 'xml') {
1397 $this->initDocument('xml');
1398 $this->elementStart('hash');
1399 $this->element('error', null, $msg);
1400 $this->element('request', null, $_SERVER['REQUEST_URI']);
1401 $this->elementEnd('hash');
1402 $this->endDocument('xml');
1404 $this->initDocument('json');
1405 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1406 print(json_encode($error_array));
1407 $this->endDocument('json');
1411 function initTwitterRss()
1414 $this->elementStart(
1418 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1419 'xmlns:georss' => 'http://www.georss.org/georss'
1422 $this->elementStart('channel');
1423 Event::handle('StartApiRss', array($this));
1426 function endTwitterRss()
1428 $this->elementEnd('channel');
1429 $this->elementEnd('rss');
1433 function initTwitterAtom()
1436 // FIXME: don't hardcode the language here!
1437 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1438 'xml:lang' => 'en-US',
1439 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1442 function endTwitterAtom()
1444 $this->elementEnd('feed');
1448 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1450 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1451 switch ($content_type) {
1453 $this->showTwitterXmlUser($profile_array);
1456 $this->showJsonObjects($profile_array);
1459 // TRANS: Client error on an API request with an unsupported data format.
1460 $this->clientError(_('Not a supported data format.'));
1466 private static function is_decimal($str)
1468 return preg_match('/^[0-9]+$/', $str);
1471 function getTargetUser($id)
1474 // Twitter supports these other ways of passing the user ID
1475 if (self::is_decimal($this->arg('id'))) {
1476 return User::getKV($this->arg('id'));
1477 } else if ($this->arg('id')) {
1478 $nickname = common_canonical_nickname($this->arg('id'));
1479 return User::getKV('nickname', $nickname);
1480 } else if ($this->arg('user_id')) {
1481 // This is to ensure that a non-numeric user_id still
1482 // overrides screen_name even if it doesn't get used
1483 if (self::is_decimal($this->arg('user_id'))) {
1484 return User::getKV('id', $this->arg('user_id'));
1486 } else if ($this->arg('screen_name')) {
1487 $nickname = common_canonical_nickname($this->arg('screen_name'));
1488 return User::getKV('nickname', $nickname);
1490 // Fall back to trying the currently authenticated user
1491 return $this->auth_user;
1494 } else if (self::is_decimal($id)) {
1495 return User::getKV($id);
1497 $nickname = common_canonical_nickname($id);
1498 return User::getKV('nickname', $nickname);
1502 function getTargetProfile($id)
1506 // Twitter supports these other ways of passing the user ID
1507 if (self::is_decimal($this->arg('id'))) {
1508 return Profile::getKV($this->arg('id'));
1509 } else if ($this->arg('id')) {
1510 // Screen names currently can only uniquely identify a local user.
1511 $nickname = common_canonical_nickname($this->arg('id'));
1512 $user = User::getKV('nickname', $nickname);
1513 return $user ? $user->getProfile() : null;
1514 } else if ($this->arg('user_id')) {
1515 // This is to ensure that a non-numeric user_id still
1516 // overrides screen_name even if it doesn't get used
1517 if (self::is_decimal($this->arg('user_id'))) {
1518 return Profile::getKV('id', $this->arg('user_id'));
1520 } else if ($this->arg('screen_name')) {
1521 $nickname = common_canonical_nickname($this->arg('screen_name'));
1522 $user = User::getKV('nickname', $nickname);
1523 return $user ? $user->getProfile() : null;
1525 } else if (self::is_decimal($id)) {
1526 return Profile::getKV($id);
1528 $nickname = common_canonical_nickname($id);
1529 $user = User::getKV('nickname', $nickname);
1530 return $user ? $user->getProfile() : null;
1534 function getTargetGroup($id)
1537 if (self::is_decimal($this->arg('id'))) {
1538 return User_group::getKV('id', $this->arg('id'));
1539 } else if ($this->arg('id')) {
1540 return User_group::getForNickname($this->arg('id'));
1541 } else if ($this->arg('group_id')) {
1542 // This is to ensure that a non-numeric group_id still
1543 // overrides group_name even if it doesn't get used
1544 if (self::is_decimal($this->arg('group_id'))) {
1545 return User_group::getKV('id', $this->arg('group_id'));
1547 } else if ($this->arg('group_name')) {
1548 return User_group::getForNickname($this->arg('group_name'));
1551 } else if (self::is_decimal($id)) {
1552 return User_group::getKV('id', $id);
1554 return User_group::getForNickname($id);
1558 function getTargetList($user=null, $id=null)
1560 $tagger = $this->getTargetUser($user);
1564 $id = $this->arg('id');
1568 if (is_numeric($id)) {
1569 $list = Profile_list::getKV('id', $id);
1571 // only if the list with the id belongs to the tagger
1572 if(empty($list) || $list->tagger != $tagger->id) {
1577 $tag = common_canonical_tag($id);
1578 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1581 if (!empty($list) && $list->private) {
1582 if ($this->auth_user->id == $list->tagger) {
1593 * Returns query argument or default value if not found. Certain
1594 * parameters used throughout the API are lightly scrubbed and
1595 * bounds checked. This overrides Action::arg().
1597 * @param string $key requested argument
1598 * @param string $def default value to return if $key is not provided
1602 function arg($key, $def=null)
1604 // XXX: Do even more input validation/scrubbing?
1606 if (array_key_exists($key, $this->args)) {
1609 $page = (int)$this->args['page'];
1610 return ($page < 1) ? 1 : $page;
1612 $count = (int)$this->args['count'];
1615 } elseif ($count > 200) {
1621 $since_id = (int)$this->args['since_id'];
1622 return ($since_id < 1) ? 0 : $since_id;
1624 $max_id = (int)$this->args['max_id'];
1625 return ($max_id < 1) ? 0 : $max_id;
1627 return parent::arg($key, $def);
1635 * Calculate the complete URI that called up this action. Used for
1636 * Atom rel="self" links. Warning: this is funky.
1638 * @return string URL a URL suitable for rel="self" Atom links
1640 function getSelfUri()
1642 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1644 $id = $this->arg('id');
1645 $aargs = array('format' => $this->format);
1650 $tag = $this->arg('tag');
1652 $aargs['tag'] = $tag;
1655 parse_str($_SERVER['QUERY_STRING'], $params);
1657 if (!empty($params)) {
1658 unset($params['p']);
1659 $pstring = http_build_query($params);
1662 $uri = common_local_url($action, $aargs);
1664 if (!empty($pstring)) {
1665 $uri .= '?' . $pstring;