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;
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
142 protected function prepare(array $args=array())
144 StatusNet::setApi(true); // reduce exception reports to aid in debugging
145 parent::prepare($args);
147 $this->format = $this->arg('format');
148 $this->callback = $this->arg('callback');
149 $this->page = (int)$this->arg('page', 1);
150 $this->count = (int)$this->arg('count', 20);
151 $this->max_id = (int)$this->arg('max_id', 0);
152 $this->since_id = (int)$this->arg('since_id', 0);
154 if ($this->arg('since')) {
155 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
158 $this->source = $this->trimmed('source');
160 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
161 $this->source = 'api';
170 * @param array $args Arguments from $_REQUEST
174 protected function handle()
176 header('Access-Control-Allow-Origin: *');
181 * Overrides XMLOutputter::element to write booleans as strings (true|false).
182 * See that method's documentation for more info.
184 * @param string $tag Element type or tagname
185 * @param array $attrs Array of element attributes, as
187 * @param string $content string content of the element
191 function element($tag, $attrs=null, $content=null)
193 if (is_bool($content)) {
194 $content = ($content ? 'true' : 'false');
197 return parent::element($tag, $attrs, $content);
200 function twitterUserArray($profile, $get_notice=false)
202 $twitter_user = array();
205 $user = $profile->getUser();
206 } catch (NoSuchUserException $e) {
210 $twitter_user['id'] = intval($profile->id);
211 $twitter_user['name'] = $profile->getBestName();
212 $twitter_user['screen_name'] = $profile->nickname;
213 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
214 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
216 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
217 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
218 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
220 // START introduced by qvitter API, not necessary for StatusNet API
221 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
223 $avatar = Avatar::getUploaded($profile);
224 $origurl = $avatar->displayUrl();
225 } catch (Exception $e) {
226 $origurl = $twitter_user['profile_image_url_profile_size'];
228 $twitter_user['profile_image_url_original'] = $origurl;
230 $twitter_user['groups_count'] = $profile->getGroupCount();
231 foreach (array('linkcolor', 'backgroundcolor') as $key) {
232 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
234 // END introduced by qvitter API, not necessary for StatusNet API
236 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
237 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
238 $twitter_user['followers_count'] = $profile->subscriberCount();
240 // Note: some profiles don't have an associated user
242 $twitter_user['friends_count'] = $profile->subscriptionCount();
244 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
246 $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
250 if (!empty($user) && $user->timezone) {
251 $timezone = $user->timezone;
255 $t->setTimezone(new DateTimeZone($timezone));
257 $twitter_user['utc_offset'] = $t->format('Z');
258 $twitter_user['time_zone'] = $timezone;
259 $twitter_user['statuses_count'] = $profile->noticeCount();
261 // Is the requesting user following this user?
262 $twitter_user['following'] = false;
263 $twitter_user['statusnet_blocking'] = false;
264 $twitter_user['notifications'] = false;
266 if (isset($this->auth_user)) {
268 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
269 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
272 $sub = Subscription::pkeyGet(array('subscriber' =>
273 $this->auth_user->id,
274 'subscribed' => $profile->id));
277 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
282 $notice = $profile->getCurrentNotice();
283 if ($notice instanceof Notice) {
285 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
289 // StatusNet-specific
291 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
293 return $twitter_user;
296 function twitterStatusArray($notice, $include_user=true)
298 $base = $this->twitterSimpleStatusArray($notice, $include_user);
300 if (!empty($notice->repeat_of)) {
301 $original = Notice::getKV('id', $notice->repeat_of);
302 if ($original instanceof Notice) {
303 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
304 $base['retweeted_status'] = $orig_array;
311 function twitterSimpleStatusArray($notice, $include_user=true)
313 $profile = $notice->getProfile();
315 $twitter_status = array();
316 $twitter_status['text'] = $notice->content;
317 $twitter_status['truncated'] = false; # Not possible on StatusNet
318 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
320 // We could just do $notice->reply_to but maybe the future holds a
321 // different story for parenting.
322 $parent = $notice->getParent();
323 $in_reply_to = $parent->id;
324 } catch (Exception $e) {
327 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
331 $ns = $notice->getSource();
333 if (!empty($ns->name) && !empty($ns->url)) {
334 $source = '<a href="'
335 . htmlspecialchars($ns->url)
336 . '" rel="nofollow">'
337 . htmlspecialchars($ns->name)
344 $twitter_status['uri'] = $notice->getUri();
345 $twitter_status['source'] = $source;
346 $twitter_status['id'] = intval($notice->id);
348 $replier_profile = null;
350 if ($notice->reply_to) {
351 $reply = Notice::getKV(intval($notice->reply_to));
353 $replier_profile = $reply->getProfile();
357 $twitter_status['in_reply_to_user_id'] =
358 ($replier_profile) ? intval($replier_profile->id) : null;
359 $twitter_status['in_reply_to_screen_name'] =
360 ($replier_profile) ? $replier_profile->nickname : null;
362 if (isset($notice->lat) && isset($notice->lon)) {
363 // This is the format that GeoJSON expects stuff to be in
364 $twitter_status['geo'] = array('type' => 'Point',
365 'coordinates' => array((float) $notice->lat,
366 (float) $notice->lon));
368 $twitter_status['geo'] = null;
371 if (!is_null($this->scoped)) {
372 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
374 $twitter_status['repeated'] = false;
378 $attachments = $notice->attachments();
380 if (!empty($attachments)) {
382 $twitter_status['attachments'] = array();
384 foreach ($attachments as $attachment) {
386 $enclosure_o = $attachment->getEnclosure();
387 $enclosure = array();
388 $enclosure['url'] = $enclosure_o->url;
389 $enclosure['mimetype'] = $enclosure_o->mimetype;
390 $enclosure['size'] = $enclosure_o->size;
391 $twitter_status['attachments'][] = $enclosure;
392 } catch (ServerException $e) {
393 // There was not enough metadata available
398 if ($include_user && $profile) {
399 // Don't get notice (recursive!)
400 $twitter_user = $this->twitterUserArray($profile, false);
401 $twitter_status['user'] = $twitter_user;
404 // StatusNet-specific
406 $twitter_status['statusnet_html'] = $notice->rendered;
407 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
409 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
410 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
411 array('include_user'=>$include_user)));
413 return $twitter_status;
416 function twitterGroupArray($group)
418 $twitter_group = array();
420 $twitter_group['id'] = intval($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['admin_count'] = $group->getAdminCount();
434 $twitter_group['member_count'] = $group->getMemberCount();
435 $twitter_group['original_logo'] = $group->original_logo;
436 $twitter_group['homepage_logo'] = $group->homepage_logo;
437 $twitter_group['stream_logo'] = $group->stream_logo;
438 $twitter_group['mini_logo'] = $group->mini_logo;
439 $twitter_group['homepage'] = $group->homepage;
440 $twitter_group['description'] = $group->description;
441 $twitter_group['location'] = $group->location;
442 $twitter_group['created'] = $this->dateTwitter($group->created);
443 $twitter_group['modified'] = $this->dateTwitter($group->modified);
445 return $twitter_group;
448 function twitterRssGroupArray($group)
451 $entry['content']=$group->description;
452 $entry['title']=$group->nickname;
453 $entry['link']=$group->permalink();
454 $entry['published']=common_date_iso8601($group->created);
455 $entry['updated']==common_date_iso8601($group->modified);
456 $taguribase = common_config('integration', 'groupuri');
457 $entry['id'] = "group:$groupuribase:$entry[link]";
459 $entry['description'] = $entry['content'];
460 $entry['pubDate'] = common_date_rfc2822($group->created);
461 $entry['guid'] = $entry['link'];
466 function twitterListArray($list)
468 $profile = Profile::getKV('id', $list->tagger);
470 $twitter_list = array();
471 $twitter_list['id'] = $list->id;
472 $twitter_list['name'] = $list->tag;
473 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
474 $twitter_list['slug'] = $list->tag;
475 $twitter_list['description'] = $list->description;
476 $twitter_list['subscriber_count'] = $list->subscriberCount();
477 $twitter_list['member_count'] = $list->taggedCount();
478 $twitter_list['uri'] = $list->getUri();
480 if (isset($this->auth_user)) {
481 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
483 $twitter_list['following'] = false;
486 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
487 $twitter_list['user'] = $this->twitterUserArray($profile, false);
489 return $twitter_list;
492 function twitterRssEntryArray($notice)
496 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
497 $profile = $notice->getProfile();
499 // We trim() to avoid extraneous whitespace in the output
501 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
502 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
503 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
504 $entry['published'] = common_date_iso8601($notice->created);
506 $taguribase = TagURI::base();
507 $entry['id'] = "tag:$taguribase:$entry[link]";
509 $entry['updated'] = $entry['published'];
510 $entry['author'] = $profile->getBestName();
513 $attachments = $notice->attachments();
514 $enclosures = array();
516 foreach ($attachments as $attachment) {
518 $enclosure_o = $attachment->getEnclosure();
519 $enclosure = array();
520 $enclosure['url'] = $enclosure_o->url;
521 $enclosure['mimetype'] = $enclosure_o->mimetype;
522 $enclosure['size'] = $enclosure_o->size;
523 $enclosures[] = $enclosure;
524 } catch (ServerException $e) {
525 // There was not enough metadata available
529 if (!empty($enclosures)) {
530 $entry['enclosures'] = $enclosures;
534 $tag = new Notice_tag();
535 $tag->notice_id = $notice->id;
537 $entry['tags']=array();
538 while ($tag->fetch()) {
539 $entry['tags'][]=$tag->tag;
545 $entry['description'] = $entry['content'];
546 $entry['pubDate'] = common_date_rfc2822($notice->created);
547 $entry['guid'] = $entry['link'];
549 if (isset($notice->lat) && isset($notice->lon)) {
550 // This is the format that GeoJSON expects stuff to be in.
551 // showGeoRSS() below uses it for XML output, so we reuse it
552 $entry['geo'] = array('type' => 'Point',
553 'coordinates' => array((float) $notice->lat,
554 (float) $notice->lon));
556 $entry['geo'] = null;
559 Event::handle('EndRssEntryArray', array($notice, &$entry));
565 function twitterRelationshipArray($source, $target)
567 $relationship = array();
569 $relationship['source'] =
570 $this->relationshipDetailsArray($source, $target);
571 $relationship['target'] =
572 $this->relationshipDetailsArray($target, $source);
574 return array('relationship' => $relationship);
577 function relationshipDetailsArray($source, $target)
581 $details['screen_name'] = $source->nickname;
582 $details['followed_by'] = $target->isSubscribed($source);
583 $details['following'] = $source->isSubscribed($target);
585 $notifications = false;
587 if ($source->isSubscribed($target)) {
588 $sub = Subscription::pkeyGet(array('subscriber' =>
589 $source->id, 'subscribed' => $target->id));
592 $notifications = ($sub->jabber || $sub->sms);
596 $details['notifications_enabled'] = $notifications;
597 $details['blocking'] = $source->hasBlocked($target);
598 $details['id'] = intval($source->id);
603 function showTwitterXmlRelationship($relationship)
605 $this->elementStart('relationship');
607 foreach($relationship as $element => $value) {
608 if ($element == 'source' || $element == 'target') {
609 $this->elementStart($element);
610 $this->showXmlRelationshipDetails($value);
611 $this->elementEnd($element);
615 $this->elementEnd('relationship');
618 function showXmlRelationshipDetails($details)
620 foreach($details as $element => $value) {
621 $this->element($element, null, $value);
625 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
629 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
631 $this->elementStart($tag, $attrs);
632 foreach($twitter_status as $element => $value) {
635 $this->showTwitterXmlUser($twitter_status['user']);
638 $this->element($element, null, common_xml_safe_str($value));
641 $this->showXmlAttachments($twitter_status['attachments']);
644 $this->showGeoXML($value);
646 case 'retweeted_status':
647 $this->showTwitterXmlStatus($value, 'retweeted_status');
650 if (strncmp($element, 'statusnet_', 10) == 0) {
651 $this->element('statusnet:'.substr($element, 10), null, $value);
653 $this->element($element, null, $value);
657 $this->elementEnd($tag);
660 function showTwitterXmlGroup($twitter_group)
662 $this->elementStart('group');
663 foreach($twitter_group as $element => $value) {
664 $this->element($element, null, $value);
666 $this->elementEnd('group');
669 function showTwitterXmlList($twitter_list)
671 $this->elementStart('list');
672 foreach($twitter_list as $element => $value) {
673 if($element == 'user') {
674 $this->showTwitterXmlUser($value, 'user');
677 $this->element($element, null, $value);
680 $this->elementEnd('list');
683 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
687 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
689 $this->elementStart($role, $attrs);
690 foreach($twitter_user as $element => $value) {
691 if ($element == 'status') {
692 $this->showTwitterXmlStatus($twitter_user['status']);
693 } else if (strncmp($element, 'statusnet_', 10) == 0) {
694 $this->element('statusnet:'.substr($element, 10), null, $value);
696 $this->element($element, null, $value);
699 $this->elementEnd($role);
702 function showXmlAttachments($attachments) {
703 if (!empty($attachments)) {
704 $this->elementStart('attachments', array('type' => 'array'));
705 foreach ($attachments as $attachment) {
707 $attrs['url'] = $attachment['url'];
708 $attrs['mimetype'] = $attachment['mimetype'];
709 $attrs['size'] = $attachment['size'];
710 $this->element('enclosure', $attrs, '');
712 $this->elementEnd('attachments');
716 function showGeoXML($geo)
720 $this->element('geo');
722 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
723 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
724 $this->elementEnd('geo');
728 function showGeoRSS($geo)
734 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
739 function showTwitterRssItem($entry)
741 $this->elementStart('item');
742 $this->element('title', null, $entry['title']);
743 $this->element('description', null, $entry['description']);
744 $this->element('pubDate', null, $entry['pubDate']);
745 $this->element('guid', null, $entry['guid']);
746 $this->element('link', null, $entry['link']);
748 // RSS only supports 1 enclosure per item
749 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
750 $enclosure = $entry['enclosures'][0];
751 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
754 if(array_key_exists('tags', $entry)){
755 foreach($entry['tags'] as $tag){
756 $this->element('category', null,$tag);
760 $this->showGeoRSS($entry['geo']);
761 $this->elementEnd('item');
764 function showJsonObjects($objects)
766 print(json_encode($objects));
769 function showSingleXmlStatus($notice)
771 $this->initDocument('xml');
772 $twitter_status = $this->twitterStatusArray($notice);
773 $this->showTwitterXmlStatus($twitter_status, 'status', true);
774 $this->endDocument('xml');
777 function showSingleAtomStatus($notice)
779 header('Content-Type: application/atom+xml; charset=utf-8');
780 print $notice->asAtomEntry(true, true, true, $this->auth_user);
783 function show_single_json_status($notice)
785 $this->initDocument('json');
786 $status = $this->twitterStatusArray($notice);
787 $this->showJsonObjects($status);
788 $this->endDocument('json');
791 function showXmlTimeline($notice)
793 $this->initDocument('xml');
794 $this->elementStart('statuses', array('type' => 'array',
795 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
797 if (is_array($notice)) {
798 $notice = new ArrayWrapper($notice);
801 while ($notice->fetch()) {
803 $twitter_status = $this->twitterStatusArray($notice);
804 $this->showTwitterXmlStatus($twitter_status);
805 } catch (Exception $e) {
806 common_log(LOG_ERR, $e->getMessage());
811 $this->elementEnd('statuses');
812 $this->endDocument('xml');
815 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
817 $this->initDocument('rss');
819 $this->element('title', null, $title);
820 $this->element('link', null, $link);
822 if (!is_null($self)) {
826 'type' => 'application/rss+xml',
833 if (!is_null($suplink)) {
834 // For FriendFeed's SUP protocol
835 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
836 'rel' => 'http://api.friendfeed.com/2008/03#sup',
838 'type' => 'application/json'));
841 if (!is_null($logo)) {
842 $this->elementStart('image');
843 $this->element('link', null, $link);
844 $this->element('title', null, $title);
845 $this->element('url', null, $logo);
846 $this->elementEnd('image');
849 $this->element('description', null, $subtitle);
850 $this->element('language', null, 'en-us');
851 $this->element('ttl', null, '40');
853 if (is_array($notice)) {
854 $notice = new ArrayWrapper($notice);
857 while ($notice->fetch()) {
859 $entry = $this->twitterRssEntryArray($notice);
860 $this->showTwitterRssItem($entry);
861 } catch (Exception $e) {
862 common_log(LOG_ERR, $e->getMessage());
863 // continue on exceptions
867 $this->endTwitterRss();
870 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
872 $this->initDocument('atom');
874 $this->element('title', null, $title);
875 $this->element('id', null, $id);
876 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
878 if (!is_null($logo)) {
879 $this->element('logo',null,$logo);
882 if (!is_null($suplink)) {
883 // For FriendFeed's SUP protocol
884 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
886 'type' => 'application/json'));
889 if (!is_null($selfuri)) {
890 $this->element('link', array('href' => $selfuri,
891 'rel' => 'self', 'type' => 'application/atom+xml'), null);
894 $this->element('updated', null, common_date_iso8601('now'));
895 $this->element('subtitle', null, $subtitle);
897 if (is_array($notice)) {
898 $notice = new ArrayWrapper($notice);
901 while ($notice->fetch()) {
903 $this->raw($notice->asAtomEntry());
904 } catch (Exception $e) {
905 common_log(LOG_ERR, $e->getMessage());
910 $this->endDocument('atom');
913 function showRssGroups($group, $title, $link, $subtitle)
915 $this->initDocument('rss');
917 $this->element('title', null, $title);
918 $this->element('link', null, $link);
919 $this->element('description', null, $subtitle);
920 $this->element('language', null, 'en-us');
921 $this->element('ttl', null, '40');
923 if (is_array($group)) {
924 foreach ($group as $g) {
925 $twitter_group = $this->twitterRssGroupArray($g);
926 $this->showTwitterRssItem($twitter_group);
929 while ($group->fetch()) {
930 $twitter_group = $this->twitterRssGroupArray($group);
931 $this->showTwitterRssItem($twitter_group);
935 $this->endTwitterRss();
938 function showTwitterAtomEntry($entry)
940 $this->elementStart('entry');
941 $this->element('title', null, common_xml_safe_str($entry['title']));
944 array('type' => 'html'),
945 common_xml_safe_str($entry['content'])
947 $this->element('id', null, $entry['id']);
948 $this->element('published', null, $entry['published']);
949 $this->element('updated', null, $entry['updated']);
950 $this->element('link', array('type' => 'text/html',
951 'href' => $entry['link'],
952 'rel' => 'alternate'));
953 $this->element('link', array('type' => $entry['avatar-type'],
954 'href' => $entry['avatar'],
956 $this->elementStart('author');
958 $this->element('name', null, $entry['author-name']);
959 $this->element('uri', null, $entry['author-uri']);
961 $this->elementEnd('author');
962 $this->elementEnd('entry');
965 function showXmlDirectMessage($dm, $namespaces=false)
969 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
971 $this->elementStart('direct_message', $attrs);
972 foreach($dm as $element => $value) {
976 $this->showTwitterXmlUser($value, $element);
979 $this->element($element, null, common_xml_safe_str($value));
982 $this->element($element, null, $value);
986 $this->elementEnd('direct_message');
989 function directMessageArray($message)
993 $from_profile = $message->getFrom();
994 $to_profile = $message->getTo();
996 $dmsg['id'] = intval($message->id);
997 $dmsg['sender_id'] = intval($from_profile->id);
998 $dmsg['text'] = trim($message->content);
999 $dmsg['recipient_id'] = intval($to_profile->id);
1000 $dmsg['created_at'] = $this->dateTwitter($message->created);
1001 $dmsg['sender_screen_name'] = $from_profile->nickname;
1002 $dmsg['recipient_screen_name'] = $to_profile->nickname;
1003 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
1004 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1009 function rssDirectMessageArray($message)
1013 $from = $message->getFrom();
1015 $entry['title'] = sprintf('Message from %1$s to %2$s',
1016 $from->nickname, $message->getTo()->nickname);
1018 $entry['content'] = common_xml_safe_str($message->rendered);
1019 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1020 $entry['published'] = common_date_iso8601($message->created);
1022 $taguribase = TagURI::base();
1024 $entry['id'] = "tag:$taguribase:$entry[link]";
1025 $entry['updated'] = $entry['published'];
1027 $entry['author-name'] = $from->getBestName();
1028 $entry['author-uri'] = $from->homepage;
1030 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1032 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1033 $entry['avatar-type'] = $avatar->mediatype;
1034 } catch (Exception $e) {
1035 $entry['avatar-type'] = 'image/png';
1038 // RSS item specific
1040 $entry['description'] = $entry['content'];
1041 $entry['pubDate'] = common_date_rfc2822($message->created);
1042 $entry['guid'] = $entry['link'];
1047 function showSingleXmlDirectMessage($message)
1049 $this->initDocument('xml');
1050 $dmsg = $this->directMessageArray($message);
1051 $this->showXmlDirectMessage($dmsg, true);
1052 $this->endDocument('xml');
1055 function showSingleJsonDirectMessage($message)
1057 $this->initDocument('json');
1058 $dmsg = $this->directMessageArray($message);
1059 $this->showJsonObjects($dmsg);
1060 $this->endDocument('json');
1063 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1065 $this->initDocument('atom');
1067 $this->element('title', null, common_xml_safe_str($title));
1068 $this->element('id', null, $id);
1069 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1071 if (!is_null($selfuri)) {
1072 $this->element('link', array('href' => $selfuri,
1073 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1076 $this->element('updated', null, common_date_iso8601('now'));
1077 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1079 if (is_array($group)) {
1080 foreach ($group as $g) {
1081 $this->raw($g->asAtomEntry());
1084 while ($group->fetch()) {
1085 $this->raw($group->asAtomEntry());
1089 $this->endDocument('atom');
1093 function showJsonTimeline($notice)
1095 $this->initDocument('json');
1097 $statuses = array();
1099 if (is_array($notice)) {
1100 $notice = new ArrayWrapper($notice);
1103 while ($notice->fetch()) {
1105 $twitter_status = $this->twitterStatusArray($notice);
1106 array_push($statuses, $twitter_status);
1107 } catch (Exception $e) {
1108 common_log(LOG_ERR, $e->getMessage());
1113 $this->showJsonObjects($statuses);
1115 $this->endDocument('json');
1118 function showJsonGroups($group)
1120 $this->initDocument('json');
1124 if (is_array($group)) {
1125 foreach ($group as $g) {
1126 $twitter_group = $this->twitterGroupArray($g);
1127 array_push($groups, $twitter_group);
1130 while ($group->fetch()) {
1131 $twitter_group = $this->twitterGroupArray($group);
1132 array_push($groups, $twitter_group);
1136 $this->showJsonObjects($groups);
1138 $this->endDocument('json');
1141 function showXmlGroups($group)
1144 $this->initDocument('xml');
1145 $this->elementStart('groups', array('type' => 'array'));
1147 if (is_array($group)) {
1148 foreach ($group as $g) {
1149 $twitter_group = $this->twitterGroupArray($g);
1150 $this->showTwitterXmlGroup($twitter_group);
1153 while ($group->fetch()) {
1154 $twitter_group = $this->twitterGroupArray($group);
1155 $this->showTwitterXmlGroup($twitter_group);
1159 $this->elementEnd('groups');
1160 $this->endDocument('xml');
1163 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1166 $this->initDocument('xml');
1167 $this->elementStart('lists_list');
1168 $this->elementStart('lists', array('type' => 'array'));
1170 if (is_array($list)) {
1171 foreach ($list as $l) {
1172 $twitter_list = $this->twitterListArray($l);
1173 $this->showTwitterXmlList($twitter_list);
1176 while ($list->fetch()) {
1177 $twitter_list = $this->twitterListArray($list);
1178 $this->showTwitterXmlList($twitter_list);
1182 $this->elementEnd('lists');
1184 $this->element('next_cursor', null, $next_cursor);
1185 $this->element('previous_cursor', null, $prev_cursor);
1187 $this->elementEnd('lists_list');
1188 $this->endDocument('xml');
1191 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1193 $this->initDocument('json');
1197 if (is_array($list)) {
1198 foreach ($list as $l) {
1199 $twitter_list = $this->twitterListArray($l);
1200 array_push($lists, $twitter_list);
1203 while ($list->fetch()) {
1204 $twitter_list = $this->twitterListArray($list);
1205 array_push($lists, $twitter_list);
1209 $lists_list = array(
1211 'next_cursor' => $next_cursor,
1212 'next_cursor_str' => strval($next_cursor),
1213 'previous_cursor' => $prev_cursor,
1214 'previous_cursor_str' => strval($prev_cursor)
1217 $this->showJsonObjects($lists_list);
1219 $this->endDocument('json');
1222 function showTwitterXmlUsers($user)
1224 $this->initDocument('xml');
1225 $this->elementStart('users', array('type' => 'array',
1226 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1228 if (is_array($user)) {
1229 foreach ($user as $u) {
1230 $twitter_user = $this->twitterUserArray($u);
1231 $this->showTwitterXmlUser($twitter_user);
1234 while ($user->fetch()) {
1235 $twitter_user = $this->twitterUserArray($user);
1236 $this->showTwitterXmlUser($twitter_user);
1240 $this->elementEnd('users');
1241 $this->endDocument('xml');
1244 function showJsonUsers($user)
1246 $this->initDocument('json');
1250 if (is_array($user)) {
1251 foreach ($user as $u) {
1252 $twitter_user = $this->twitterUserArray($u);
1253 array_push($users, $twitter_user);
1256 while ($user->fetch()) {
1257 $twitter_user = $this->twitterUserArray($user);
1258 array_push($users, $twitter_user);
1262 $this->showJsonObjects($users);
1264 $this->endDocument('json');
1267 function showSingleJsonGroup($group)
1269 $this->initDocument('json');
1270 $twitter_group = $this->twitterGroupArray($group);
1271 $this->showJsonObjects($twitter_group);
1272 $this->endDocument('json');
1275 function showSingleXmlGroup($group)
1277 $this->initDocument('xml');
1278 $twitter_group = $this->twitterGroupArray($group);
1279 $this->showTwitterXmlGroup($twitter_group);
1280 $this->endDocument('xml');
1283 function showSingleJsonList($list)
1285 $this->initDocument('json');
1286 $twitter_list = $this->twitterListArray($list);
1287 $this->showJsonObjects($twitter_list);
1288 $this->endDocument('json');
1291 function showSingleXmlList($list)
1293 $this->initDocument('xml');
1294 $twitter_list = $this->twitterListArray($list);
1295 $this->showTwitterXmlList($twitter_list);
1296 $this->endDocument('xml');
1299 function dateTwitter($dt)
1301 $dateStr = date('d F Y H:i:s', strtotime($dt));
1302 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1303 $d->setTimezone(new DateTimeZone(common_timezone()));
1304 return $d->format('D M d H:i:s O Y');
1307 function initDocument($type='xml')
1311 header('Content-Type: application/xml; charset=utf-8');
1315 header('Content-Type: application/json; charset=utf-8');
1317 // Check for JSONP callback
1318 if (isset($this->callback)) {
1319 print $this->callback . '(';
1323 header("Content-Type: application/rss+xml; charset=utf-8");
1324 $this->initTwitterRss();
1327 header('Content-Type: application/atom+xml; charset=utf-8');
1328 $this->initTwitterAtom();
1331 // TRANS: Client error on an API request with an unsupported data format.
1332 $this->clientError(_('Not a supported data format.'));
1338 function endDocument($type='xml')
1345 // Check for JSONP callback
1346 if (isset($this->callback)) {
1351 $this->endTwitterRss();
1354 $this->endTwitterRss();
1357 // TRANS: Client error on an API request with an unsupported data format.
1358 $this->clientError(_('Not a supported data format.'));
1363 function initTwitterRss()
1366 $this->elementStart(
1370 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1371 'xmlns:georss' => 'http://www.georss.org/georss'
1374 $this->elementStart('channel');
1375 Event::handle('StartApiRss', array($this));
1378 function endTwitterRss()
1380 $this->elementEnd('channel');
1381 $this->elementEnd('rss');
1385 function initTwitterAtom()
1388 // FIXME: don't hardcode the language here!
1389 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1390 'xml:lang' => 'en-US',
1391 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1394 function endTwitterAtom()
1396 $this->elementEnd('feed');
1400 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1402 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1403 switch ($content_type) {
1405 $this->showTwitterXmlUser($profile_array);
1408 $this->showJsonObjects($profile_array);
1411 // TRANS: Client error on an API request with an unsupported data format.
1412 $this->clientError(_('Not a supported data format.'));
1417 private static function is_decimal($str)
1419 return preg_match('/^[0-9]+$/', $str);
1422 function getTargetUser($id)
1425 // Twitter supports these other ways of passing the user ID
1426 if (self::is_decimal($this->arg('id'))) {
1427 return User::getKV($this->arg('id'));
1428 } else if ($this->arg('id')) {
1429 $nickname = common_canonical_nickname($this->arg('id'));
1430 return User::getKV('nickname', $nickname);
1431 } else if ($this->arg('user_id')) {
1432 // This is to ensure that a non-numeric user_id still
1433 // overrides screen_name even if it doesn't get used
1434 if (self::is_decimal($this->arg('user_id'))) {
1435 return User::getKV('id', $this->arg('user_id'));
1437 } else if ($this->arg('screen_name')) {
1438 $nickname = common_canonical_nickname($this->arg('screen_name'));
1439 return User::getKV('nickname', $nickname);
1441 // Fall back to trying the currently authenticated user
1442 return $this->auth_user;
1445 } else if (self::is_decimal($id)) {
1446 return User::getKV($id);
1448 $nickname = common_canonical_nickname($id);
1449 return User::getKV('nickname', $nickname);
1453 function getTargetProfile($id)
1457 // Twitter supports these other ways of passing the user ID
1458 if (self::is_decimal($this->arg('id'))) {
1459 return Profile::getKV($this->arg('id'));
1460 } else if ($this->arg('id')) {
1461 // Screen names currently can only uniquely identify a local user.
1462 $nickname = common_canonical_nickname($this->arg('id'));
1463 $user = User::getKV('nickname', $nickname);
1464 return $user ? $user->getProfile() : null;
1465 } else if ($this->arg('user_id')) {
1466 // This is to ensure that a non-numeric user_id still
1467 // overrides screen_name even if it doesn't get used
1468 if (self::is_decimal($this->arg('user_id'))) {
1469 return Profile::getKV('id', $this->arg('user_id'));
1471 } else if ($this->arg('screen_name')) {
1472 $nickname = common_canonical_nickname($this->arg('screen_name'));
1473 $user = User::getKV('nickname', $nickname);
1474 return $user instanceof User ? $user->getProfile() : null;
1476 // Fall back to trying the currently authenticated user
1477 return $this->scoped;
1479 } else if (self::is_decimal($id)) {
1480 return Profile::getKV($id);
1482 $nickname = common_canonical_nickname($id);
1483 $user = User::getKV('nickname', $nickname);
1484 return $user ? $user->getProfile() : null;
1488 function getTargetGroup($id)
1491 if (self::is_decimal($this->arg('id'))) {
1492 return User_group::getKV('id', $this->arg('id'));
1493 } else if ($this->arg('id')) {
1494 return User_group::getForNickname($this->arg('id'));
1495 } else if ($this->arg('group_id')) {
1496 // This is to ensure that a non-numeric group_id still
1497 // overrides group_name even if it doesn't get used
1498 if (self::is_decimal($this->arg('group_id'))) {
1499 return User_group::getKV('id', $this->arg('group_id'));
1501 } else if ($this->arg('group_name')) {
1502 return User_group::getForNickname($this->arg('group_name'));
1505 } else if (self::is_decimal($id)) {
1506 return User_group::getKV('id', $id);
1507 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1508 return User_group::getKV('uri', urldecode($this->arg('uri')));
1510 return User_group::getForNickname($id);
1514 function getTargetList($user=null, $id=null)
1516 $tagger = $this->getTargetUser($user);
1520 $id = $this->arg('id');
1524 if (is_numeric($id)) {
1525 $list = Profile_list::getKV('id', $id);
1527 // only if the list with the id belongs to the tagger
1528 if(empty($list) || $list->tagger != $tagger->id) {
1533 $tag = common_canonical_tag($id);
1534 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1537 if (!empty($list) && $list->private) {
1538 if ($this->auth_user->id == $list->tagger) {
1549 * Returns query argument or default value if not found. Certain
1550 * parameters used throughout the API are lightly scrubbed and
1551 * bounds checked. This overrides Action::arg().
1553 * @param string $key requested argument
1554 * @param string $def default value to return if $key is not provided
1558 function arg($key, $def=null)
1560 // XXX: Do even more input validation/scrubbing?
1562 if (array_key_exists($key, $this->args)) {
1565 $page = (int)$this->args['page'];
1566 return ($page < 1) ? 1 : $page;
1568 $count = (int)$this->args['count'];
1571 } elseif ($count > 200) {
1577 $since_id = (int)$this->args['since_id'];
1578 return ($since_id < 1) ? 0 : $since_id;
1580 $max_id = (int)$this->args['max_id'];
1581 return ($max_id < 1) ? 0 : $max_id;
1583 return parent::arg($key, $def);
1591 * Calculate the complete URI that called up this action. Used for
1592 * Atom rel="self" links. Warning: this is funky.
1594 * @return string URL a URL suitable for rel="self" Atom links
1596 function getSelfUri()
1598 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1600 $id = $this->arg('id');
1601 $aargs = array('format' => $this->format);
1606 $tag = $this->arg('tag');
1608 $aargs['tag'] = $tag;
1611 parse_str($_SERVER['QUERY_STRING'], $params);
1613 if (!empty($params)) {
1614 unset($params['p']);
1615 $pstring = http_build_query($params);
1618 $uri = common_local_url($action, $aargs);
1620 if (!empty($pstring)) {
1621 $uri .= '?' . $pstring;