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 (now a plugin)
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;
129 var $since_id = null;
131 var $callback = null;
134 var $access = self::READ_ONLY; // read (default) or read-write
136 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
141 * @param array $args Web and URL arguments
143 * @return boolean false if user doesn't exist
145 protected function prepare(array $args=array())
147 GNUsocial::setApi(true); // reduce exception reports to aid in debugging
148 parent::prepare($args);
150 $this->format = $this->arg('format');
151 $this->callback = $this->arg('callback');
152 $this->page = (int)$this->arg('page', 1);
153 $this->count = (int)$this->arg('count', 20);
154 $this->max_id = (int)$this->arg('max_id', 0);
155 $this->since_id = (int)$this->arg('since_id', 0);
157 // These two are not used everywhere, mainly just AtompubAction extensions
158 $this->offset = ($this->page-1) * $this->count;
159 $this->limit = $this->count + 1;
161 if ($this->arg('since')) {
162 header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
165 $this->source = $this->trimmed('source');
167 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
168 $this->source = 'api';
177 * @param array $args Arguments from $_REQUEST
181 protected function handle()
183 header('Access-Control-Allow-Origin: *');
188 * Overrides XMLOutputter::element to write booleans as strings (true|false).
189 * See that method's documentation for more info.
191 * @param string $tag Element type or tagname
192 * @param array $attrs Array of element attributes, as
194 * @param string $content string content of the element
198 function element($tag, $attrs=null, $content=null)
200 if (is_bool($content)) {
201 $content = ($content ? 'true' : 'false');
204 return parent::element($tag, $attrs, $content);
207 function twitterUserArray($profile, $get_notice=false)
209 $twitter_user = array();
212 $user = $profile->getUser();
213 } catch (NoSuchUserException $e) {
217 $twitter_user['id'] = $profile->getID();
218 $twitter_user['name'] = $profile->getBestName();
219 $twitter_user['screen_name'] = $profile->getNickname();
220 $twitter_user['location'] = $profile->location;
221 $twitter_user['description'] = $profile->getDescription();
223 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
224 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
225 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
227 // START introduced by qvitter API, not necessary for StatusNet API
228 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
230 $avatar = Avatar::getUploaded($profile);
231 $origurl = $avatar->displayUrl();
232 } catch (Exception $e) {
233 $origurl = $twitter_user['profile_image_url_profile_size'];
235 $twitter_user['profile_image_url_original'] = $origurl;
237 $twitter_user['groups_count'] = $profile->getGroupCount();
238 foreach (array('linkcolor', 'backgroundcolor') as $key) {
239 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
241 // END introduced by qvitter API, not necessary for StatusNet API
243 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
244 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
245 $twitter_user['followers_count'] = $profile->subscriberCount();
247 // Note: some profiles don't have an associated user
249 $twitter_user['friends_count'] = $profile->subscriptionCount();
251 $twitter_user['created_at'] = self::dateTwitter($profile->created);
255 if (!empty($user) && $user->timezone) {
256 $timezone = $user->timezone;
260 $t->setTimezone(new DateTimeZone($timezone));
262 $twitter_user['utc_offset'] = $t->format('Z');
263 $twitter_user['time_zone'] = $timezone;
264 $twitter_user['statuses_count'] = $profile->noticeCount();
266 // Is the requesting user following this user?
267 // These values might actually also mean "unknown". Ambiguity issues?
268 $twitter_user['following'] = false;
269 $twitter_user['statusnet_blocking'] = false;
270 $twitter_user['notifications'] = false;
272 if ($this->scoped instanceof Profile) {
274 $sub = Subscription::getSubscription($this->scoped, $profile);
276 $twitter_user['following'] = true;
277 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
278 } catch (NoResultException $e) {
279 // well, the values are already false...
281 $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
285 $notice = $profile->getCurrentNotice();
286 if ($notice instanceof Notice) {
288 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
292 // StatusNet-specific
294 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
296 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
297 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
299 return $twitter_user;
302 function twitterStatusArray($notice, $include_user=true)
304 $base = $this->twitterSimpleStatusArray($notice, $include_user);
306 // FIXME: MOVE TO SHARE PLUGIN
307 if (!empty($notice->repeat_of)) {
308 $original = Notice::getKV('id', $notice->repeat_of);
309 if ($original instanceof Notice) {
310 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
311 $base['retweeted_status'] = $orig_array;
318 function twitterSimpleStatusArray($notice, $include_user=true)
320 $profile = $notice->getProfile();
322 $twitter_status = array();
323 $twitter_status['text'] = $notice->content;
324 $twitter_status['truncated'] = false; # Not possible on StatusNet
325 $twitter_status['created_at'] = self::dateTwitter($notice->created);
327 // We could just do $notice->reply_to but maybe the future holds a
328 // different story for parenting.
329 $parent = $notice->getParent();
330 $in_reply_to = $parent->id;
331 } catch (NoParentNoticeException $e) {
333 } catch (NoResultException $e) {
334 // the in_reply_to message has probably been deleted
337 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
342 $ns = $notice->getSource();
343 if ($ns instanceof Notice_source) {
345 if (!empty($ns->url)) {
346 $source_link = $ns->url;
347 if (!empty($ns->name)) {
353 $twitter_status['uri'] = $notice->getUri();
354 $twitter_status['source'] = $source;
355 $twitter_status['source_link'] = $source_link;
356 $twitter_status['id'] = intval($notice->id);
358 $replier_profile = null;
360 if ($notice->reply_to) {
361 $reply = Notice::getKV(intval($notice->reply_to));
363 $replier_profile = $reply->getProfile();
367 $twitter_status['in_reply_to_user_id'] =
368 ($replier_profile) ? intval($replier_profile->id) : null;
369 $twitter_status['in_reply_to_screen_name'] =
370 ($replier_profile) ? $replier_profile->nickname : null;
373 $notloc = Notice_location::locFromStored($notice);
374 // This is the format that GeoJSON expects stuff to be in
375 $twitter_status['geo'] = array('type' => 'Point',
376 'coordinates' => array((float) $notloc->lat,
377 (float) $notloc->lon));
378 } catch (ServerException $e) {
379 $twitter_status['geo'] = null;
383 $attachments = $notice->attachments();
385 if (!empty($attachments)) {
387 $twitter_status['attachments'] = array();
389 foreach ($attachments as $attachment) {
391 $enclosure_o = $attachment->getEnclosure();
392 $enclosure = array();
393 $enclosure['url'] = $enclosure_o->url;
394 $enclosure['mimetype'] = $enclosure_o->mimetype;
395 $enclosure['size'] = $enclosure_o->size;
396 $twitter_status['attachments'][] = $enclosure;
397 } catch (ServerException $e) {
398 // There was not enough metadata available
403 if ($include_user && $profile) {
404 // Don't get notice (recursive!)
405 $twitter_user = $this->twitterUserArray($profile, false);
406 $twitter_status['user'] = $twitter_user;
409 // StatusNet-specific
411 $twitter_status['statusnet_html'] = $notice->getRendered();
412 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
414 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
415 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
416 array('include_user'=>$include_user)));
418 return $twitter_status;
421 function twitterGroupArray($group)
423 $twitter_group = array();
425 $twitter_group['id'] = intval($group->id);
426 $twitter_group['url'] = $group->permalink();
427 $twitter_group['nickname'] = $group->nickname;
428 $twitter_group['fullname'] = $group->fullname;
430 if ($this->scoped instanceof Profile) {
431 $twitter_group['member'] = $this->scoped->isMember($group);
432 $twitter_group['blocked'] = Group_block::isBlocked(
438 $twitter_group['admin_count'] = $group->getAdminCount();
439 $twitter_group['member_count'] = $group->getMemberCount();
440 $twitter_group['original_logo'] = $group->original_logo;
441 $twitter_group['homepage_logo'] = $group->homepage_logo;
442 $twitter_group['stream_logo'] = $group->stream_logo;
443 $twitter_group['mini_logo'] = $group->mini_logo;
444 $twitter_group['homepage'] = $group->homepage;
445 $twitter_group['description'] = $group->description;
446 $twitter_group['location'] = $group->location;
447 $twitter_group['created'] = self::dateTwitter($group->created);
448 $twitter_group['modified'] = self::dateTwitter($group->modified);
450 return $twitter_group;
453 function twitterRssGroupArray($group)
456 $entry['content']=$group->description;
457 $entry['title']=$group->nickname;
458 $entry['link']=$group->permalink();
459 $entry['published']=common_date_iso8601($group->created);
460 $entry['updated']==common_date_iso8601($group->modified);
461 $taguribase = common_config('integration', 'groupuri');
462 $entry['id'] = "group:$groupuribase:$entry[link]";
464 $entry['description'] = $entry['content'];
465 $entry['pubDate'] = common_date_rfc2822($group->created);
466 $entry['guid'] = $entry['link'];
471 function twitterListArray($list)
473 $profile = Profile::getKV('id', $list->tagger);
475 $twitter_list = array();
476 $twitter_list['id'] = $list->id;
477 $twitter_list['name'] = $list->tag;
478 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
479 $twitter_list['slug'] = $list->tag;
480 $twitter_list['description'] = $list->description;
481 $twitter_list['subscriber_count'] = $list->subscriberCount();
482 $twitter_list['member_count'] = $list->taggedCount();
483 $twitter_list['uri'] = $list->getUri();
485 if ($this->scoped instanceof Profile) {
486 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
488 $twitter_list['following'] = false;
491 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
492 $twitter_list['user'] = $this->twitterUserArray($profile, false);
494 return $twitter_list;
497 function twitterRssEntryArray($notice)
501 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
502 $profile = $notice->getProfile();
504 // We trim() to avoid extraneous whitespace in the output
506 $entry['content'] = common_xml_safe_str(trim($notice->getRendered()));
507 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
508 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
509 $entry['published'] = common_date_iso8601($notice->created);
511 $taguribase = TagURI::base();
512 $entry['id'] = "tag:$taguribase:$entry[link]";
514 $entry['updated'] = $entry['published'];
515 $entry['author'] = $profile->getBestName();
518 $attachments = $notice->attachments();
519 $enclosures = array();
521 foreach ($attachments as $attachment) {
523 $enclosure_o = $attachment->getEnclosure();
524 $enclosure = array();
525 $enclosure['url'] = $enclosure_o->url;
526 $enclosure['mimetype'] = $enclosure_o->mimetype;
527 $enclosure['size'] = $enclosure_o->size;
528 $enclosures[] = $enclosure;
529 } catch (ServerException $e) {
530 // There was not enough metadata available
534 if (!empty($enclosures)) {
535 $entry['enclosures'] = $enclosures;
539 $tag = new Notice_tag();
540 $tag->notice_id = $notice->id;
542 $entry['tags']=array();
543 while ($tag->fetch()) {
544 $entry['tags'][]=$tag->tag;
550 $entry['description'] = $entry['content'];
551 $entry['pubDate'] = common_date_rfc2822($notice->created);
552 $entry['guid'] = $entry['link'];
555 $notloc = Notice_location::locFromStored($notice);
556 // This is the format that GeoJSON expects stuff to be in.
557 // showGeoRSS() below uses it for XML output, so we reuse it
558 $entry['geo'] = array('type' => 'Point',
559 'coordinates' => array((float) $notloc->lat,
560 (float) $notloc->lon));
561 } catch (ServerException $e) {
562 $entry['geo'] = null;
565 Event::handle('EndRssEntryArray', array($notice, &$entry));
571 function twitterRelationshipArray($source, $target)
573 $relationship = array();
575 $relationship['source'] =
576 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
577 $relationship['target'] =
578 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
580 return array('relationship' => $relationship);
583 function relationshipDetailsArray(Profile $source, Profile $target)
587 $details['screen_name'] = $source->getNickname();
588 $details['followed_by'] = $target->isSubscribed($source);
591 $sub = Subscription::getSubscription($source, $target);
592 $details['following'] = true;
593 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
594 } catch (NoResultException $e) {
595 $details['following'] = false;
596 $details['notifications_enabled'] = false;
599 $details['blocking'] = $source->hasBlocked($target);
600 $details['id'] = intval($source->id);
605 function showTwitterXmlRelationship($relationship)
607 $this->elementStart('relationship');
609 foreach($relationship as $element => $value) {
610 if ($element == 'source' || $element == 'target') {
611 $this->elementStart($element);
612 $this->showXmlRelationshipDetails($value);
613 $this->elementEnd($element);
617 $this->elementEnd('relationship');
620 function showXmlRelationshipDetails($details)
622 foreach($details as $element => $value) {
623 $this->element($element, null, $value);
627 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
631 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
633 $this->elementStart($tag, $attrs);
634 foreach($twitter_status as $element => $value) {
637 $this->showTwitterXmlUser($twitter_status['user']);
640 $this->element($element, null, common_xml_safe_str($value));
643 $this->showXmlAttachments($twitter_status['attachments']);
646 $this->showGeoXML($value);
648 case 'retweeted_status':
649 // FIXME: MOVE TO SHARE PLUGIN
650 $this->showTwitterXmlStatus($value, 'retweeted_status');
653 if (strncmp($element, 'statusnet_', 10) == 0) {
654 if ($element === 'statusnet_in_groups' && is_array($value)) {
655 // QVITTERFIX because it would cause an array to be sent as $value
656 // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
657 $value = json_encode($value);
659 $this->element('statusnet:'.substr($element, 10), null, $value);
661 $this->element($element, null, $value);
665 $this->elementEnd($tag);
668 function showTwitterXmlGroup($twitter_group)
670 $this->elementStart('group');
671 foreach($twitter_group as $element => $value) {
672 $this->element($element, null, $value);
674 $this->elementEnd('group');
677 function showTwitterXmlList($twitter_list)
679 $this->elementStart('list');
680 foreach($twitter_list as $element => $value) {
681 if($element == 'user') {
682 $this->showTwitterXmlUser($value, 'user');
685 $this->element($element, null, $value);
688 $this->elementEnd('list');
691 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
695 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
697 $this->elementStart($role, $attrs);
698 foreach($twitter_user as $element => $value) {
699 if ($element == 'status') {
700 $this->showTwitterXmlStatus($twitter_user['status']);
701 } else if (strncmp($element, 'statusnet_', 10) == 0) {
702 $this->element('statusnet:'.substr($element, 10), null, $value);
704 $this->element($element, null, $value);
707 $this->elementEnd($role);
710 function showXmlAttachments($attachments) {
711 if (!empty($attachments)) {
712 $this->elementStart('attachments', array('type' => 'array'));
713 foreach ($attachments as $attachment) {
715 $attrs['url'] = $attachment['url'];
716 $attrs['mimetype'] = $attachment['mimetype'];
717 $attrs['size'] = $attachment['size'];
718 $this->element('enclosure', $attrs, '');
720 $this->elementEnd('attachments');
724 function showGeoXML($geo)
728 $this->element('geo');
730 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
731 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
732 $this->elementEnd('geo');
736 function showGeoRSS($geo)
742 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
747 function showTwitterRssItem($entry)
749 $this->elementStart('item');
750 $this->element('title', null, $entry['title']);
751 $this->element('description', null, $entry['description']);
752 $this->element('pubDate', null, $entry['pubDate']);
753 $this->element('guid', null, $entry['guid']);
754 $this->element('link', null, $entry['link']);
756 // RSS only supports 1 enclosure per item
757 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
758 $enclosure = $entry['enclosures'][0];
759 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
762 if(array_key_exists('tags', $entry)){
763 foreach($entry['tags'] as $tag){
764 $this->element('category', null,$tag);
768 $this->showGeoRSS($entry['geo']);
769 $this->elementEnd('item');
772 function showJsonObjects($objects)
774 $json_objects = json_encode($objects);
775 if($json_objects === false) {
776 $this->clientError(_('JSON encoding failed. Error: ').json_last_error_msg());
783 function showSingleXmlStatus($notice)
785 $this->initDocument('xml');
786 $twitter_status = $this->twitterStatusArray($notice);
787 $this->showTwitterXmlStatus($twitter_status, 'status', true);
788 $this->endDocument('xml');
791 function showSingleAtomStatus($notice)
793 header('Content-Type: application/atom+xml; charset=utf-8');
794 print $notice->asAtomEntry(true, true, true, $this->scoped);
797 function show_single_json_status($notice)
799 $this->initDocument('json');
800 $status = $this->twitterStatusArray($notice);
801 $this->showJsonObjects($status);
802 $this->endDocument('json');
805 function showXmlTimeline($notice)
807 $this->initDocument('xml');
808 $this->elementStart('statuses', array('type' => 'array',
809 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
811 if (is_array($notice)) {
812 //FIXME: make everything calling showJsonTimeline use only Notice objects
814 foreach ($notice as $n) {
815 $ids[] = $n->getID();
817 $notice = Notice::multiGet('id', $ids);
820 while ($notice->fetch()) {
822 $twitter_status = $this->twitterStatusArray($notice);
823 $this->showTwitterXmlStatus($twitter_status);
824 } catch (Exception $e) {
825 common_log(LOG_ERR, $e->getMessage());
830 $this->elementEnd('statuses');
831 $this->endDocument('xml');
834 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
836 $this->initDocument('rss');
838 $this->element('title', null, $title);
839 $this->element('link', null, $link);
841 if (!is_null($self)) {
845 'type' => 'application/rss+xml',
852 if (!is_null($suplink)) {
853 // For FriendFeed's SUP protocol
854 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
855 'rel' => 'http://api.friendfeed.com/2008/03#sup',
857 'type' => 'application/json'));
860 if (!is_null($logo)) {
861 $this->elementStart('image');
862 $this->element('link', null, $link);
863 $this->element('title', null, $title);
864 $this->element('url', null, $logo);
865 $this->elementEnd('image');
868 $this->element('description', null, $subtitle);
869 $this->element('language', null, 'en-us');
870 $this->element('ttl', null, '40');
872 if (is_array($notice)) {
873 //FIXME: make everything calling showJsonTimeline use only Notice objects
875 foreach ($notice as $n) {
876 $ids[] = $n->getID();
878 $notice = Notice::multiGet('id', $ids);
881 while ($notice->fetch()) {
883 $entry = $this->twitterRssEntryArray($notice);
884 $this->showTwitterRssItem($entry);
885 } catch (Exception $e) {
886 common_log(LOG_ERR, $e->getMessage());
887 // continue on exceptions
891 $this->endTwitterRss();
894 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
896 $this->initDocument('atom');
898 $this->element('title', null, $title);
899 $this->element('id', null, $id);
900 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
902 if (!is_null($logo)) {
903 $this->element('logo',null,$logo);
906 if (!is_null($suplink)) {
907 // For FriendFeed's SUP protocol
908 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
910 'type' => 'application/json'));
913 if (!is_null($selfuri)) {
914 $this->element('link', array('href' => $selfuri,
915 'rel' => 'self', 'type' => 'application/atom+xml'), null);
918 $this->element('updated', null, common_date_iso8601('now'));
919 $this->element('subtitle', null, $subtitle);
921 if (is_array($notice)) {
922 //FIXME: make everything calling showJsonTimeline use only Notice objects
924 foreach ($notice as $n) {
925 $ids[] = $n->getID();
927 $notice = Notice::multiGet('id', $ids);
930 while ($notice->fetch()) {
932 $this->raw($notice->asAtomEntry());
933 } catch (Exception $e) {
934 common_log(LOG_ERR, $e->getMessage());
939 $this->endDocument('atom');
942 function showRssGroups($group, $title, $link, $subtitle)
944 $this->initDocument('rss');
946 $this->element('title', null, $title);
947 $this->element('link', null, $link);
948 $this->element('description', null, $subtitle);
949 $this->element('language', null, 'en-us');
950 $this->element('ttl', null, '40');
952 if (is_array($group)) {
953 foreach ($group as $g) {
954 $twitter_group = $this->twitterRssGroupArray($g);
955 $this->showTwitterRssItem($twitter_group);
958 while ($group->fetch()) {
959 $twitter_group = $this->twitterRssGroupArray($group);
960 $this->showTwitterRssItem($twitter_group);
964 $this->endTwitterRss();
967 function showTwitterAtomEntry($entry)
969 $this->elementStart('entry');
970 $this->element('title', null, common_xml_safe_str($entry['title']));
973 array('type' => 'html'),
974 common_xml_safe_str($entry['content'])
976 $this->element('id', null, $entry['id']);
977 $this->element('published', null, $entry['published']);
978 $this->element('updated', null, $entry['updated']);
979 $this->element('link', array('type' => 'text/html',
980 'href' => $entry['link'],
981 'rel' => 'alternate'));
982 $this->element('link', array('type' => $entry['avatar-type'],
983 'href' => $entry['avatar'],
985 $this->elementStart('author');
987 $this->element('name', null, $entry['author-name']);
988 $this->element('uri', null, $entry['author-uri']);
990 $this->elementEnd('author');
991 $this->elementEnd('entry');
994 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
996 $this->initDocument('atom');
998 $this->element('title', null, common_xml_safe_str($title));
999 $this->element('id', null, $id);
1000 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1002 if (!is_null($selfuri)) {
1003 $this->element('link', array('href' => $selfuri,
1004 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1007 $this->element('updated', null, common_date_iso8601('now'));
1008 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1010 if (is_array($group)) {
1011 foreach ($group as $g) {
1012 $this->raw($g->asAtomEntry());
1015 while ($group->fetch()) {
1016 $this->raw($group->asAtomEntry());
1020 $this->endDocument('atom');
1024 function showJsonTimeline($notice)
1026 $this->initDocument('json');
1028 $statuses = array();
1030 if (is_array($notice)) {
1031 //FIXME: make everything calling showJsonTimeline use only Notice objects
1033 foreach ($notice as $n) {
1034 $ids[] = $n->getID();
1036 $notice = Notice::multiGet('id', $ids);
1039 while ($notice->fetch()) {
1041 $twitter_status = $this->twitterStatusArray($notice);
1042 array_push($statuses, $twitter_status);
1043 } catch (Exception $e) {
1044 common_log(LOG_ERR, $e->getMessage());
1049 $this->showJsonObjects($statuses);
1051 $this->endDocument('json');
1054 function showJsonGroups($group)
1056 $this->initDocument('json');
1060 if (is_array($group)) {
1061 foreach ($group as $g) {
1062 $twitter_group = $this->twitterGroupArray($g);
1063 array_push($groups, $twitter_group);
1066 while ($group->fetch()) {
1067 $twitter_group = $this->twitterGroupArray($group);
1068 array_push($groups, $twitter_group);
1072 $this->showJsonObjects($groups);
1074 $this->endDocument('json');
1077 function showXmlGroups($group)
1080 $this->initDocument('xml');
1081 $this->elementStart('groups', array('type' => 'array'));
1083 if (is_array($group)) {
1084 foreach ($group as $g) {
1085 $twitter_group = $this->twitterGroupArray($g);
1086 $this->showTwitterXmlGroup($twitter_group);
1089 while ($group->fetch()) {
1090 $twitter_group = $this->twitterGroupArray($group);
1091 $this->showTwitterXmlGroup($twitter_group);
1095 $this->elementEnd('groups');
1096 $this->endDocument('xml');
1099 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1102 $this->initDocument('xml');
1103 $this->elementStart('lists_list');
1104 $this->elementStart('lists', array('type' => 'array'));
1106 if (is_array($list)) {
1107 foreach ($list as $l) {
1108 $twitter_list = $this->twitterListArray($l);
1109 $this->showTwitterXmlList($twitter_list);
1112 while ($list->fetch()) {
1113 $twitter_list = $this->twitterListArray($list);
1114 $this->showTwitterXmlList($twitter_list);
1118 $this->elementEnd('lists');
1120 $this->element('next_cursor', null, $next_cursor);
1121 $this->element('previous_cursor', null, $prev_cursor);
1123 $this->elementEnd('lists_list');
1124 $this->endDocument('xml');
1127 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1129 $this->initDocument('json');
1133 if (is_array($list)) {
1134 foreach ($list as $l) {
1135 $twitter_list = $this->twitterListArray($l);
1136 array_push($lists, $twitter_list);
1139 while ($list->fetch()) {
1140 $twitter_list = $this->twitterListArray($list);
1141 array_push($lists, $twitter_list);
1145 $lists_list = array(
1147 'next_cursor' => $next_cursor,
1148 'next_cursor_str' => strval($next_cursor),
1149 'previous_cursor' => $prev_cursor,
1150 'previous_cursor_str' => strval($prev_cursor)
1153 $this->showJsonObjects($lists_list);
1155 $this->endDocument('json');
1158 function showTwitterXmlUsers($user)
1160 $this->initDocument('xml');
1161 $this->elementStart('users', array('type' => 'array',
1162 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1164 if (is_array($user)) {
1165 foreach ($user as $u) {
1166 $twitter_user = $this->twitterUserArray($u);
1167 $this->showTwitterXmlUser($twitter_user);
1170 while ($user->fetch()) {
1171 $twitter_user = $this->twitterUserArray($user);
1172 $this->showTwitterXmlUser($twitter_user);
1176 $this->elementEnd('users');
1177 $this->endDocument('xml');
1180 function showJsonUsers($user)
1182 $this->initDocument('json');
1186 if (is_array($user)) {
1187 foreach ($user as $u) {
1188 $twitter_user = $this->twitterUserArray($u);
1189 array_push($users, $twitter_user);
1192 while ($user->fetch()) {
1193 $twitter_user = $this->twitterUserArray($user);
1194 array_push($users, $twitter_user);
1198 $this->showJsonObjects($users);
1200 $this->endDocument('json');
1203 function showSingleJsonGroup($group)
1205 $this->initDocument('json');
1206 $twitter_group = $this->twitterGroupArray($group);
1207 $this->showJsonObjects($twitter_group);
1208 $this->endDocument('json');
1211 function showSingleXmlGroup($group)
1213 $this->initDocument('xml');
1214 $twitter_group = $this->twitterGroupArray($group);
1215 $this->showTwitterXmlGroup($twitter_group);
1216 $this->endDocument('xml');
1219 function showSingleJsonList($list)
1221 $this->initDocument('json');
1222 $twitter_list = $this->twitterListArray($list);
1223 $this->showJsonObjects($twitter_list);
1224 $this->endDocument('json');
1227 function showSingleXmlList($list)
1229 $this->initDocument('xml');
1230 $twitter_list = $this->twitterListArray($list);
1231 $this->showTwitterXmlList($twitter_list);
1232 $this->endDocument('xml');
1235 static function dateTwitter($dt)
1237 $dateStr = date('d F Y H:i:s', strtotime($dt));
1238 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1239 $d->setTimezone(new DateTimeZone(common_timezone()));
1240 return $d->format('D M d H:i:s O Y');
1243 function initDocument($type='xml')
1247 header('Content-Type: application/xml; charset=utf-8');
1251 header('Content-Type: application/json; charset=utf-8');
1253 // Check for JSONP callback
1254 if (isset($this->callback)) {
1255 print $this->callback . '(';
1259 header("Content-Type: application/rss+xml; charset=utf-8");
1260 $this->initTwitterRss();
1263 header('Content-Type: application/atom+xml; charset=utf-8');
1264 $this->initTwitterAtom();
1267 // TRANS: Client error on an API request with an unsupported data format.
1268 $this->clientError(_('Not a supported data format.'));
1274 function endDocument($type='xml')
1281 // Check for JSONP callback
1282 if (isset($this->callback)) {
1287 $this->endTwitterRss();
1290 $this->endTwitterRss();
1293 // TRANS: Client error on an API request with an unsupported data format.
1294 $this->clientError(_('Not a supported data format.'));
1299 function initTwitterRss()
1302 $this->elementStart(
1306 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1307 'xmlns:georss' => 'http://www.georss.org/georss'
1310 $this->elementStart('channel');
1311 Event::handle('StartApiRss', array($this));
1314 function endTwitterRss()
1316 $this->elementEnd('channel');
1317 $this->elementEnd('rss');
1321 function initTwitterAtom()
1324 // FIXME: don't hardcode the language here!
1325 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1326 'xml:lang' => 'en-US',
1327 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1330 function endTwitterAtom()
1332 $this->elementEnd('feed');
1336 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1338 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1339 switch ($content_type) {
1341 $this->showTwitterXmlUser($profile_array);
1344 $this->showJsonObjects($profile_array);
1347 // TRANS: Client error on an API request with an unsupported data format.
1348 $this->clientError(_('Not a supported data format.'));
1353 private static function is_decimal($str)
1355 return preg_match('/^[0-9]+$/', $str);
1358 function getTargetUser($id)
1361 // Twitter supports these other ways of passing the user ID
1362 if (self::is_decimal($this->arg('id'))) {
1363 return User::getKV($this->arg('id'));
1364 } else if ($this->arg('id')) {
1365 $nickname = common_canonical_nickname($this->arg('id'));
1366 return User::getKV('nickname', $nickname);
1367 } else if ($this->arg('user_id')) {
1368 // This is to ensure that a non-numeric user_id still
1369 // overrides screen_name even if it doesn't get used
1370 if (self::is_decimal($this->arg('user_id'))) {
1371 return User::getKV('id', $this->arg('user_id'));
1373 } else if ($this->arg('screen_name')) {
1374 $nickname = common_canonical_nickname($this->arg('screen_name'));
1375 return User::getKV('nickname', $nickname);
1377 // Fall back to trying the currently authenticated user
1378 return $this->scoped->getUser();
1381 } else if (self::is_decimal($id)) {
1382 return User::getKV($id);
1384 $nickname = common_canonical_nickname($id);
1385 return User::getKV('nickname', $nickname);
1389 function getTargetProfile($id)
1393 // Twitter supports these other ways of passing the user ID
1394 if (self::is_decimal($this->arg('id'))) {
1395 return Profile::getKV($this->arg('id'));
1396 } else if ($this->arg('id')) {
1397 // Screen names currently can only uniquely identify a local user.
1398 $nickname = common_canonical_nickname($this->arg('id'));
1399 $user = User::getKV('nickname', $nickname);
1400 return $user ? $user->getProfile() : null;
1401 } else if ($this->arg('user_id')) {
1402 // This is to ensure that a non-numeric user_id still
1403 // overrides screen_name even if it doesn't get used
1404 if (self::is_decimal($this->arg('user_id'))) {
1405 return Profile::getKV('id', $this->arg('user_id'));
1407 } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1408 $nickname = common_canonical_nickname($this->arg('screen_name'));
1409 $user = User::getByNickname($nickname);
1410 return $user->getProfile();
1412 // Fall back to trying the currently authenticated user
1413 return $this->scoped;
1415 } else if (self::is_decimal($id) && intval($id) > 0) {
1416 return Profile::getByID($id);
1418 // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1419 $nickname = common_canonical_nickname($id);
1420 $user = User::getByNickname($nickname);
1421 return $user->getProfile();
1425 function getTargetGroup($id)
1428 if (self::is_decimal($this->arg('id'))) {
1429 return User_group::getKV('id', $this->arg('id'));
1430 } else if ($this->arg('id')) {
1431 return User_group::getForNickname($this->arg('id'));
1432 } else if ($this->arg('group_id')) {
1433 // This is to ensure that a non-numeric group_id still
1434 // overrides group_name even if it doesn't get used
1435 if (self::is_decimal($this->arg('group_id'))) {
1436 return User_group::getKV('id', $this->arg('group_id'));
1438 } else if ($this->arg('group_name')) {
1439 return User_group::getForNickname($this->arg('group_name'));
1442 } else if (self::is_decimal($id)) {
1443 return User_group::getKV('id', $id);
1444 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1445 return User_group::getKV('uri', urldecode($this->arg('uri')));
1447 return User_group::getForNickname($id);
1451 function getTargetList($user=null, $id=null)
1453 $tagger = $this->getTargetUser($user);
1457 $id = $this->arg('id');
1461 if (is_numeric($id)) {
1462 $list = Profile_list::getKV('id', $id);
1464 // only if the list with the id belongs to the tagger
1465 if(empty($list) || $list->tagger != $tagger->id) {
1470 $tag = common_canonical_tag($id);
1471 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1474 if (!empty($list) && $list->private) {
1475 if ($this->scoped->id == $list->tagger) {
1486 * Returns query argument or default value if not found. Certain
1487 * parameters used throughout the API are lightly scrubbed and
1488 * bounds checked. This overrides Action::arg().
1490 * @param string $key requested argument
1491 * @param string $def default value to return if $key is not provided
1495 function arg($key, $def=null)
1497 // XXX: Do even more input validation/scrubbing?
1499 if (array_key_exists($key, $this->args)) {
1502 $page = (int)$this->args['page'];
1503 return ($page < 1) ? 1 : $page;
1505 $count = (int)$this->args['count'];
1508 } elseif ($count > 200) {
1514 $since_id = (int)$this->args['since_id'];
1515 return ($since_id < 1) ? 0 : $since_id;
1517 $max_id = (int)$this->args['max_id'];
1518 return ($max_id < 1) ? 0 : $max_id;
1520 return parent::arg($key, $def);
1528 * Calculate the complete URI that called up this action. Used for
1529 * Atom rel="self" links. Warning: this is funky.
1531 * @return string URL a URL suitable for rel="self" Atom links
1533 function getSelfUri()
1535 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1537 $id = $this->arg('id');
1538 $aargs = array('format' => $this->format);
1543 $user = $this->arg('user');
1544 if (!empty($user)) {
1545 $aargs['user'] = $user;
1548 $tag = $this->arg('tag');
1550 $aargs['tag'] = $tag;
1553 parse_str($_SERVER['QUERY_STRING'], $params);
1555 if (!empty($params)) {
1556 unset($params['p']);
1557 $pstring = http_build_query($params);
1560 $uri = common_local_url($action, $aargs);
1562 if (!empty($pstring)) {
1563 $uri .= '?' . $pstring;