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 (!empty($original)) {
303 $original_array = $this->twitterSimpleStatusArray($original, $include_user);
304 $base['retweeted_status'] = $original_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['favorited'] = $this->scoped->hasFave($notice);
373 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
375 $twitter_status['favorited'] = false;
376 $twitter_status['repeated'] = false;
380 $attachments = $notice->attachments();
382 if (!empty($attachments)) {
384 $twitter_status['attachments'] = array();
386 foreach ($attachments as $attachment) {
388 $enclosure_o = $attachment->getEnclosure();
389 $enclosure = array();
390 $enclosure['url'] = $enclosure_o->url;
391 $enclosure['mimetype'] = $enclosure_o->mimetype;
392 $enclosure['size'] = $enclosure_o->size;
393 $twitter_status['attachments'][] = $enclosure;
394 } catch (ServerException $e) {
395 // There was not enough metadata available
400 if ($include_user && $profile) {
401 // Don't get notice (recursive!)
402 $twitter_user = $this->twitterUserArray($profile, false);
403 $twitter_status['user'] = $twitter_user;
406 // StatusNet-specific
408 $twitter_status['statusnet_html'] = $notice->rendered;
409 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
411 return $twitter_status;
414 function twitterGroupArray($group)
416 $twitter_group = array();
418 $twitter_group['id'] = intval($group->id);
419 $twitter_group['url'] = $group->permalink();
420 $twitter_group['nickname'] = $group->nickname;
421 $twitter_group['fullname'] = $group->fullname;
423 if (isset($this->auth_user)) {
424 $twitter_group['member'] = $this->auth_user->isMember($group);
425 $twitter_group['blocked'] = Group_block::isBlocked(
427 $this->auth_user->getProfile()
431 $twitter_group['admin_count'] = $group->getAdminCount();
432 $twitter_group['member_count'] = $group->getMemberCount();
433 $twitter_group['original_logo'] = $group->original_logo;
434 $twitter_group['homepage_logo'] = $group->homepage_logo;
435 $twitter_group['stream_logo'] = $group->stream_logo;
436 $twitter_group['mini_logo'] = $group->mini_logo;
437 $twitter_group['homepage'] = $group->homepage;
438 $twitter_group['description'] = $group->description;
439 $twitter_group['location'] = $group->location;
440 $twitter_group['created'] = $this->dateTwitter($group->created);
441 $twitter_group['modified'] = $this->dateTwitter($group->modified);
443 return $twitter_group;
446 function twitterRssGroupArray($group)
449 $entry['content']=$group->description;
450 $entry['title']=$group->nickname;
451 $entry['link']=$group->permalink();
452 $entry['published']=common_date_iso8601($group->created);
453 $entry['updated']==common_date_iso8601($group->modified);
454 $taguribase = common_config('integration', 'groupuri');
455 $entry['id'] = "group:$groupuribase:$entry[link]";
457 $entry['description'] = $entry['content'];
458 $entry['pubDate'] = common_date_rfc2822($group->created);
459 $entry['guid'] = $entry['link'];
464 function twitterListArray($list)
466 $profile = Profile::getKV('id', $list->tagger);
468 $twitter_list = array();
469 $twitter_list['id'] = $list->id;
470 $twitter_list['name'] = $list->tag;
471 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
472 $twitter_list['slug'] = $list->tag;
473 $twitter_list['description'] = $list->description;
474 $twitter_list['subscriber_count'] = $list->subscriberCount();
475 $twitter_list['member_count'] = $list->taggedCount();
476 $twitter_list['uri'] = $list->getUri();
478 if (isset($this->auth_user)) {
479 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
481 $twitter_list['following'] = false;
484 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
485 $twitter_list['user'] = $this->twitterUserArray($profile, false);
487 return $twitter_list;
490 function twitterRssEntryArray($notice)
494 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
495 $profile = $notice->getProfile();
497 // We trim() to avoid extraneous whitespace in the output
499 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
500 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
501 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
502 $entry['published'] = common_date_iso8601($notice->created);
504 $taguribase = TagURI::base();
505 $entry['id'] = "tag:$taguribase:$entry[link]";
507 $entry['updated'] = $entry['published'];
508 $entry['author'] = $profile->getBestName();
511 $attachments = $notice->attachments();
512 $enclosures = array();
514 foreach ($attachments as $attachment) {
516 $enclosure_o = $attachment->getEnclosure();
517 $enclosure = array();
518 $enclosure['url'] = $enclosure_o->url;
519 $enclosure['mimetype'] = $enclosure_o->mimetype;
520 $enclosure['size'] = $enclosure_o->size;
521 $enclosures[] = $enclosure;
522 } catch (ServerException $e) {
523 // There was not enough metadata available
527 if (!empty($enclosures)) {
528 $entry['enclosures'] = $enclosures;
532 $tag = new Notice_tag();
533 $tag->notice_id = $notice->id;
535 $entry['tags']=array();
536 while ($tag->fetch()) {
537 $entry['tags'][]=$tag->tag;
543 $entry['description'] = $entry['content'];
544 $entry['pubDate'] = common_date_rfc2822($notice->created);
545 $entry['guid'] = $entry['link'];
547 if (isset($notice->lat) && isset($notice->lon)) {
548 // This is the format that GeoJSON expects stuff to be in.
549 // showGeoRSS() below uses it for XML output, so we reuse it
550 $entry['geo'] = array('type' => 'Point',
551 'coordinates' => array((float) $notice->lat,
552 (float) $notice->lon));
554 $entry['geo'] = null;
557 Event::handle('EndRssEntryArray', array($notice, &$entry));
563 function twitterRelationshipArray($source, $target)
565 $relationship = array();
567 $relationship['source'] =
568 $this->relationshipDetailsArray($source, $target);
569 $relationship['target'] =
570 $this->relationshipDetailsArray($target, $source);
572 return array('relationship' => $relationship);
575 function relationshipDetailsArray($source, $target)
579 $details['screen_name'] = $source->nickname;
580 $details['followed_by'] = $target->isSubscribed($source);
581 $details['following'] = $source->isSubscribed($target);
583 $notifications = false;
585 if ($source->isSubscribed($target)) {
586 $sub = Subscription::pkeyGet(array('subscriber' =>
587 $source->id, 'subscribed' => $target->id));
590 $notifications = ($sub->jabber || $sub->sms);
594 $details['notifications_enabled'] = $notifications;
595 $details['blocking'] = $source->hasBlocked($target);
596 $details['id'] = intval($source->id);
601 function showTwitterXmlRelationship($relationship)
603 $this->elementStart('relationship');
605 foreach($relationship as $element => $value) {
606 if ($element == 'source' || $element == 'target') {
607 $this->elementStart($element);
608 $this->showXmlRelationshipDetails($value);
609 $this->elementEnd($element);
613 $this->elementEnd('relationship');
616 function showXmlRelationshipDetails($details)
618 foreach($details as $element => $value) {
619 $this->element($element, null, $value);
623 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
627 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
629 $this->elementStart($tag, $attrs);
630 foreach($twitter_status as $element => $value) {
633 $this->showTwitterXmlUser($twitter_status['user']);
636 $this->element($element, null, common_xml_safe_str($value));
639 $this->showXmlAttachments($twitter_status['attachments']);
642 $this->showGeoXML($value);
644 case 'retweeted_status':
645 $this->showTwitterXmlStatus($value, 'retweeted_status');
648 if (strncmp($element, 'statusnet_', 10) == 0) {
649 $this->element('statusnet:'.substr($element, 10), null, $value);
651 $this->element($element, null, $value);
655 $this->elementEnd($tag);
658 function showTwitterXmlGroup($twitter_group)
660 $this->elementStart('group');
661 foreach($twitter_group as $element => $value) {
662 $this->element($element, null, $value);
664 $this->elementEnd('group');
667 function showTwitterXmlList($twitter_list)
669 $this->elementStart('list');
670 foreach($twitter_list as $element => $value) {
671 if($element == 'user') {
672 $this->showTwitterXmlUser($value, 'user');
675 $this->element($element, null, $value);
678 $this->elementEnd('list');
681 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
685 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
687 $this->elementStart($role, $attrs);
688 foreach($twitter_user as $element => $value) {
689 if ($element == 'status') {
690 $this->showTwitterXmlStatus($twitter_user['status']);
691 } else if (strncmp($element, 'statusnet_', 10) == 0) {
692 $this->element('statusnet:'.substr($element, 10), null, $value);
694 $this->element($element, null, $value);
697 $this->elementEnd($role);
700 function showXmlAttachments($attachments) {
701 if (!empty($attachments)) {
702 $this->elementStart('attachments', array('type' => 'array'));
703 foreach ($attachments as $attachment) {
705 $attrs['url'] = $attachment['url'];
706 $attrs['mimetype'] = $attachment['mimetype'];
707 $attrs['size'] = $attachment['size'];
708 $this->element('enclosure', $attrs, '');
710 $this->elementEnd('attachments');
714 function showGeoXML($geo)
718 $this->element('geo');
720 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
721 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
722 $this->elementEnd('geo');
726 function showGeoRSS($geo)
732 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
737 function showTwitterRssItem($entry)
739 $this->elementStart('item');
740 $this->element('title', null, $entry['title']);
741 $this->element('description', null, $entry['description']);
742 $this->element('pubDate', null, $entry['pubDate']);
743 $this->element('guid', null, $entry['guid']);
744 $this->element('link', null, $entry['link']);
746 // RSS only supports 1 enclosure per item
747 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
748 $enclosure = $entry['enclosures'][0];
749 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
752 if(array_key_exists('tags', $entry)){
753 foreach($entry['tags'] as $tag){
754 $this->element('category', null,$tag);
758 $this->showGeoRSS($entry['geo']);
759 $this->elementEnd('item');
762 function showJsonObjects($objects)
764 print(json_encode($objects));
767 function showSingleXmlStatus($notice)
769 $this->initDocument('xml');
770 $twitter_status = $this->twitterStatusArray($notice);
771 $this->showTwitterXmlStatus($twitter_status, 'status', true);
772 $this->endDocument('xml');
775 function showSingleAtomStatus($notice)
777 header('Content-Type: application/atom+xml; charset=utf-8');
778 print $notice->asAtomEntry(true, true, true, $this->auth_user);
781 function show_single_json_status($notice)
783 $this->initDocument('json');
784 $status = $this->twitterStatusArray($notice);
785 $this->showJsonObjects($status);
786 $this->endDocument('json');
789 function showXmlTimeline($notice)
791 $this->initDocument('xml');
792 $this->elementStart('statuses', array('type' => 'array',
793 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
795 if (is_array($notice)) {
796 $notice = new ArrayWrapper($notice);
799 while ($notice->fetch()) {
801 $twitter_status = $this->twitterStatusArray($notice);
802 $this->showTwitterXmlStatus($twitter_status);
803 } catch (Exception $e) {
804 common_log(LOG_ERR, $e->getMessage());
809 $this->elementEnd('statuses');
810 $this->endDocument('xml');
813 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
815 $this->initDocument('rss');
817 $this->element('title', null, $title);
818 $this->element('link', null, $link);
820 if (!is_null($self)) {
824 'type' => 'application/rss+xml',
831 if (!is_null($suplink)) {
832 // For FriendFeed's SUP protocol
833 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
834 'rel' => 'http://api.friendfeed.com/2008/03#sup',
836 'type' => 'application/json'));
839 if (!is_null($logo)) {
840 $this->elementStart('image');
841 $this->element('link', null, $link);
842 $this->element('title', null, $title);
843 $this->element('url', null, $logo);
844 $this->elementEnd('image');
847 $this->element('description', null, $subtitle);
848 $this->element('language', null, 'en-us');
849 $this->element('ttl', null, '40');
851 if (is_array($notice)) {
852 $notice = new ArrayWrapper($notice);
855 while ($notice->fetch()) {
857 $entry = $this->twitterRssEntryArray($notice);
858 $this->showTwitterRssItem($entry);
859 } catch (Exception $e) {
860 common_log(LOG_ERR, $e->getMessage());
861 // continue on exceptions
865 $this->endTwitterRss();
868 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
870 $this->initDocument('atom');
872 $this->element('title', null, $title);
873 $this->element('id', null, $id);
874 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
876 if (!is_null($logo)) {
877 $this->element('logo',null,$logo);
880 if (!is_null($suplink)) {
881 // For FriendFeed's SUP protocol
882 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
884 'type' => 'application/json'));
887 if (!is_null($selfuri)) {
888 $this->element('link', array('href' => $selfuri,
889 'rel' => 'self', 'type' => 'application/atom+xml'), null);
892 $this->element('updated', null, common_date_iso8601('now'));
893 $this->element('subtitle', null, $subtitle);
895 if (is_array($notice)) {
896 $notice = new ArrayWrapper($notice);
899 while ($notice->fetch()) {
901 $this->raw($notice->asAtomEntry());
902 } catch (Exception $e) {
903 common_log(LOG_ERR, $e->getMessage());
908 $this->endDocument('atom');
911 function showRssGroups($group, $title, $link, $subtitle)
913 $this->initDocument('rss');
915 $this->element('title', null, $title);
916 $this->element('link', null, $link);
917 $this->element('description', null, $subtitle);
918 $this->element('language', null, 'en-us');
919 $this->element('ttl', null, '40');
921 if (is_array($group)) {
922 foreach ($group as $g) {
923 $twitter_group = $this->twitterRssGroupArray($g);
924 $this->showTwitterRssItem($twitter_group);
927 while ($group->fetch()) {
928 $twitter_group = $this->twitterRssGroupArray($group);
929 $this->showTwitterRssItem($twitter_group);
933 $this->endTwitterRss();
936 function showTwitterAtomEntry($entry)
938 $this->elementStart('entry');
939 $this->element('title', null, common_xml_safe_str($entry['title']));
942 array('type' => 'html'),
943 common_xml_safe_str($entry['content'])
945 $this->element('id', null, $entry['id']);
946 $this->element('published', null, $entry['published']);
947 $this->element('updated', null, $entry['updated']);
948 $this->element('link', array('type' => 'text/html',
949 'href' => $entry['link'],
950 'rel' => 'alternate'));
951 $this->element('link', array('type' => $entry['avatar-type'],
952 'href' => $entry['avatar'],
954 $this->elementStart('author');
956 $this->element('name', null, $entry['author-name']);
957 $this->element('uri', null, $entry['author-uri']);
959 $this->elementEnd('author');
960 $this->elementEnd('entry');
963 function showXmlDirectMessage($dm, $namespaces=false)
967 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
969 $this->elementStart('direct_message', $attrs);
970 foreach($dm as $element => $value) {
974 $this->showTwitterXmlUser($value, $element);
977 $this->element($element, null, common_xml_safe_str($value));
980 $this->element($element, null, $value);
984 $this->elementEnd('direct_message');
987 function directMessageArray($message)
991 $from_profile = $message->getFrom();
992 $to_profile = $message->getTo();
994 $dmsg['id'] = intval($message->id);
995 $dmsg['sender_id'] = intval($from_profile->id);
996 $dmsg['text'] = trim($message->content);
997 $dmsg['recipient_id'] = intval($to_profile->id);
998 $dmsg['created_at'] = $this->dateTwitter($message->created);
999 $dmsg['sender_screen_name'] = $from_profile->nickname;
1000 $dmsg['recipient_screen_name'] = $to_profile->nickname;
1001 $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
1002 $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1007 function rssDirectMessageArray($message)
1011 $from = $message->getFrom();
1013 $entry['title'] = sprintf('Message from %1$s to %2$s',
1014 $from->nickname, $message->getTo()->nickname);
1016 $entry['content'] = common_xml_safe_str($message->rendered);
1017 $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1018 $entry['published'] = common_date_iso8601($message->created);
1020 $taguribase = TagURI::base();
1022 $entry['id'] = "tag:$taguribase:$entry[link]";
1023 $entry['updated'] = $entry['published'];
1025 $entry['author-name'] = $from->getBestName();
1026 $entry['author-uri'] = $from->homepage;
1028 $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1030 $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1031 $entry['avatar-type'] = $avatar->mediatype;
1032 } catch (Exception $e) {
1033 $entry['avatar-type'] = 'image/png';
1036 // RSS item specific
1038 $entry['description'] = $entry['content'];
1039 $entry['pubDate'] = common_date_rfc2822($message->created);
1040 $entry['guid'] = $entry['link'];
1045 function showSingleXmlDirectMessage($message)
1047 $this->initDocument('xml');
1048 $dmsg = $this->directMessageArray($message);
1049 $this->showXmlDirectMessage($dmsg, true);
1050 $this->endDocument('xml');
1053 function showSingleJsonDirectMessage($message)
1055 $this->initDocument('json');
1056 $dmsg = $this->directMessageArray($message);
1057 $this->showJsonObjects($dmsg);
1058 $this->endDocument('json');
1061 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1063 $this->initDocument('atom');
1065 $this->element('title', null, common_xml_safe_str($title));
1066 $this->element('id', null, $id);
1067 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1069 if (!is_null($selfuri)) {
1070 $this->element('link', array('href' => $selfuri,
1071 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1074 $this->element('updated', null, common_date_iso8601('now'));
1075 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1077 if (is_array($group)) {
1078 foreach ($group as $g) {
1079 $this->raw($g->asAtomEntry());
1082 while ($group->fetch()) {
1083 $this->raw($group->asAtomEntry());
1087 $this->endDocument('atom');
1091 function showJsonTimeline($notice)
1093 $this->initDocument('json');
1095 $statuses = array();
1097 if (is_array($notice)) {
1098 $notice = new ArrayWrapper($notice);
1101 while ($notice->fetch()) {
1103 $twitter_status = $this->twitterStatusArray($notice);
1104 array_push($statuses, $twitter_status);
1105 } catch (Exception $e) {
1106 common_log(LOG_ERR, $e->getMessage());
1111 $this->showJsonObjects($statuses);
1113 $this->endDocument('json');
1116 function showJsonGroups($group)
1118 $this->initDocument('json');
1122 if (is_array($group)) {
1123 foreach ($group as $g) {
1124 $twitter_group = $this->twitterGroupArray($g);
1125 array_push($groups, $twitter_group);
1128 while ($group->fetch()) {
1129 $twitter_group = $this->twitterGroupArray($group);
1130 array_push($groups, $twitter_group);
1134 $this->showJsonObjects($groups);
1136 $this->endDocument('json');
1139 function showXmlGroups($group)
1142 $this->initDocument('xml');
1143 $this->elementStart('groups', array('type' => 'array'));
1145 if (is_array($group)) {
1146 foreach ($group as $g) {
1147 $twitter_group = $this->twitterGroupArray($g);
1148 $this->showTwitterXmlGroup($twitter_group);
1151 while ($group->fetch()) {
1152 $twitter_group = $this->twitterGroupArray($group);
1153 $this->showTwitterXmlGroup($twitter_group);
1157 $this->elementEnd('groups');
1158 $this->endDocument('xml');
1161 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1164 $this->initDocument('xml');
1165 $this->elementStart('lists_list');
1166 $this->elementStart('lists', array('type' => 'array'));
1168 if (is_array($list)) {
1169 foreach ($list as $l) {
1170 $twitter_list = $this->twitterListArray($l);
1171 $this->showTwitterXmlList($twitter_list);
1174 while ($list->fetch()) {
1175 $twitter_list = $this->twitterListArray($list);
1176 $this->showTwitterXmlList($twitter_list);
1180 $this->elementEnd('lists');
1182 $this->element('next_cursor', null, $next_cursor);
1183 $this->element('previous_cursor', null, $prev_cursor);
1185 $this->elementEnd('lists_list');
1186 $this->endDocument('xml');
1189 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1191 $this->initDocument('json');
1195 if (is_array($list)) {
1196 foreach ($list as $l) {
1197 $twitter_list = $this->twitterListArray($l);
1198 array_push($lists, $twitter_list);
1201 while ($list->fetch()) {
1202 $twitter_list = $this->twitterListArray($list);
1203 array_push($lists, $twitter_list);
1207 $lists_list = array(
1209 'next_cursor' => $next_cursor,
1210 'next_cursor_str' => strval($next_cursor),
1211 'previous_cursor' => $prev_cursor,
1212 'previous_cursor_str' => strval($prev_cursor)
1215 $this->showJsonObjects($lists_list);
1217 $this->endDocument('json');
1220 function showTwitterXmlUsers($user)
1222 $this->initDocument('xml');
1223 $this->elementStart('users', array('type' => 'array',
1224 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1226 if (is_array($user)) {
1227 foreach ($user as $u) {
1228 $twitter_user = $this->twitterUserArray($u);
1229 $this->showTwitterXmlUser($twitter_user);
1232 while ($user->fetch()) {
1233 $twitter_user = $this->twitterUserArray($user);
1234 $this->showTwitterXmlUser($twitter_user);
1238 $this->elementEnd('users');
1239 $this->endDocument('xml');
1242 function showJsonUsers($user)
1244 $this->initDocument('json');
1248 if (is_array($user)) {
1249 foreach ($user as $u) {
1250 $twitter_user = $this->twitterUserArray($u);
1251 array_push($users, $twitter_user);
1254 while ($user->fetch()) {
1255 $twitter_user = $this->twitterUserArray($user);
1256 array_push($users, $twitter_user);
1260 $this->showJsonObjects($users);
1262 $this->endDocument('json');
1265 function showSingleJsonGroup($group)
1267 $this->initDocument('json');
1268 $twitter_group = $this->twitterGroupArray($group);
1269 $this->showJsonObjects($twitter_group);
1270 $this->endDocument('json');
1273 function showSingleXmlGroup($group)
1275 $this->initDocument('xml');
1276 $twitter_group = $this->twitterGroupArray($group);
1277 $this->showTwitterXmlGroup($twitter_group);
1278 $this->endDocument('xml');
1281 function showSingleJsonList($list)
1283 $this->initDocument('json');
1284 $twitter_list = $this->twitterListArray($list);
1285 $this->showJsonObjects($twitter_list);
1286 $this->endDocument('json');
1289 function showSingleXmlList($list)
1291 $this->initDocument('xml');
1292 $twitter_list = $this->twitterListArray($list);
1293 $this->showTwitterXmlList($twitter_list);
1294 $this->endDocument('xml');
1297 function dateTwitter($dt)
1299 $dateStr = date('d F Y H:i:s', strtotime($dt));
1300 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1301 $d->setTimezone(new DateTimeZone(common_timezone()));
1302 return $d->format('D M d H:i:s O Y');
1305 function initDocument($type='xml')
1309 header('Content-Type: application/xml; charset=utf-8');
1313 header('Content-Type: application/json; charset=utf-8');
1315 // Check for JSONP callback
1316 if (isset($this->callback)) {
1317 print $this->callback . '(';
1321 header("Content-Type: application/rss+xml; charset=utf-8");
1322 $this->initTwitterRss();
1325 header('Content-Type: application/atom+xml; charset=utf-8');
1326 $this->initTwitterAtom();
1329 // TRANS: Client error on an API request with an unsupported data format.
1330 $this->clientError(_('Not a supported data format.'));
1336 function endDocument($type='xml')
1343 // Check for JSONP callback
1344 if (isset($this->callback)) {
1349 $this->endTwitterRss();
1352 $this->endTwitterRss();
1355 // TRANS: Client error on an API request with an unsupported data format.
1356 $this->clientError(_('Not a supported data format.'));
1361 function initTwitterRss()
1364 $this->elementStart(
1368 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1369 'xmlns:georss' => 'http://www.georss.org/georss'
1372 $this->elementStart('channel');
1373 Event::handle('StartApiRss', array($this));
1376 function endTwitterRss()
1378 $this->elementEnd('channel');
1379 $this->elementEnd('rss');
1383 function initTwitterAtom()
1386 // FIXME: don't hardcode the language here!
1387 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1388 'xml:lang' => 'en-US',
1389 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1392 function endTwitterAtom()
1394 $this->elementEnd('feed');
1398 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1400 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1401 switch ($content_type) {
1403 $this->showTwitterXmlUser($profile_array);
1406 $this->showJsonObjects($profile_array);
1409 // TRANS: Client error on an API request with an unsupported data format.
1410 $this->clientError(_('Not a supported data format.'));
1415 private static function is_decimal($str)
1417 return preg_match('/^[0-9]+$/', $str);
1420 function getTargetUser($id)
1423 // Twitter supports these other ways of passing the user ID
1424 if (self::is_decimal($this->arg('id'))) {
1425 return User::getKV($this->arg('id'));
1426 } else if ($this->arg('id')) {
1427 $nickname = common_canonical_nickname($this->arg('id'));
1428 return User::getKV('nickname', $nickname);
1429 } else if ($this->arg('user_id')) {
1430 // This is to ensure that a non-numeric user_id still
1431 // overrides screen_name even if it doesn't get used
1432 if (self::is_decimal($this->arg('user_id'))) {
1433 return User::getKV('id', $this->arg('user_id'));
1435 } else if ($this->arg('screen_name')) {
1436 $nickname = common_canonical_nickname($this->arg('screen_name'));
1437 return User::getKV('nickname', $nickname);
1439 // Fall back to trying the currently authenticated user
1440 return $this->auth_user;
1443 } else if (self::is_decimal($id)) {
1444 return User::getKV($id);
1446 $nickname = common_canonical_nickname($id);
1447 return User::getKV('nickname', $nickname);
1451 function getTargetProfile($id)
1455 // Twitter supports these other ways of passing the user ID
1456 if (self::is_decimal($this->arg('id'))) {
1457 return Profile::getKV($this->arg('id'));
1458 } else if ($this->arg('id')) {
1459 // Screen names currently can only uniquely identify a local user.
1460 $nickname = common_canonical_nickname($this->arg('id'));
1461 $user = User::getKV('nickname', $nickname);
1462 return $user ? $user->getProfile() : null;
1463 } else if ($this->arg('user_id')) {
1464 // This is to ensure that a non-numeric user_id still
1465 // overrides screen_name even if it doesn't get used
1466 if (self::is_decimal($this->arg('user_id'))) {
1467 return Profile::getKV('id', $this->arg('user_id'));
1469 } else if ($this->arg('screen_name')) {
1470 $nickname = common_canonical_nickname($this->arg('screen_name'));
1471 $user = User::getKV('nickname', $nickname);
1472 return $user instanceof User ? $user->getProfile() : null;
1474 // Fall back to trying the currently authenticated user
1475 return $this->scoped;
1477 } else if (self::is_decimal($id)) {
1478 return Profile::getKV($id);
1480 $nickname = common_canonical_nickname($id);
1481 $user = User::getKV('nickname', $nickname);
1482 return $user ? $user->getProfile() : null;
1486 function getTargetGroup($id)
1489 if (self::is_decimal($this->arg('id'))) {
1490 return User_group::getKV('id', $this->arg('id'));
1491 } else if ($this->arg('id')) {
1492 return User_group::getForNickname($this->arg('id'));
1493 } else if ($this->arg('group_id')) {
1494 // This is to ensure that a non-numeric group_id still
1495 // overrides group_name even if it doesn't get used
1496 if (self::is_decimal($this->arg('group_id'))) {
1497 return User_group::getKV('id', $this->arg('group_id'));
1499 } else if ($this->arg('group_name')) {
1500 return User_group::getForNickname($this->arg('group_name'));
1503 } else if (self::is_decimal($id)) {
1504 return User_group::getKV('id', $id);
1505 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1506 return User_group::getKV('uri', urldecode($this->arg('uri')));
1508 return User_group::getForNickname($id);
1512 function getTargetList($user=null, $id=null)
1514 $tagger = $this->getTargetUser($user);
1518 $id = $this->arg('id');
1522 if (is_numeric($id)) {
1523 $list = Profile_list::getKV('id', $id);
1525 // only if the list with the id belongs to the tagger
1526 if(empty($list) || $list->tagger != $tagger->id) {
1531 $tag = common_canonical_tag($id);
1532 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1535 if (!empty($list) && $list->private) {
1536 if ($this->auth_user->id == $list->tagger) {
1547 * Returns query argument or default value if not found. Certain
1548 * parameters used throughout the API are lightly scrubbed and
1549 * bounds checked. This overrides Action::arg().
1551 * @param string $key requested argument
1552 * @param string $def default value to return if $key is not provided
1556 function arg($key, $def=null)
1558 // XXX: Do even more input validation/scrubbing?
1560 if (array_key_exists($key, $this->args)) {
1563 $page = (int)$this->args['page'];
1564 return ($page < 1) ? 1 : $page;
1566 $count = (int)$this->args['count'];
1569 } elseif ($count > 200) {
1575 $since_id = (int)$this->args['since_id'];
1576 return ($since_id < 1) ? 0 : $since_id;
1578 $max_id = (int)$this->args['max_id'];
1579 return ($max_id < 1) ? 0 : $max_id;
1581 return parent::arg($key, $def);
1589 * Calculate the complete URI that called up this action. Used for
1590 * Atom rel="self" links. Warning: this is funky.
1592 * @return string URL a URL suitable for rel="self" Atom links
1594 function getSelfUri()
1596 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1598 $id = $this->arg('id');
1599 $aargs = array('format' => $this->format);
1604 $tag = $this->arg('tag');
1606 $aargs['tag'] = $tag;
1609 parse_str($_SERVER['QUERY_STRING'], $params);
1611 if (!empty($params)) {
1612 unset($params['p']);
1613 $pstring = http_build_query($params);
1616 $uri = common_local_url($action, $aargs);
1618 if (!empty($pstring)) {
1619 $uri .= '?' . $pstring;