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
106 * Contains most of the Twitter-compatible API output functions.
110 * @author Craig Andrews <candrews@integralblue.com>
111 * @author Dan Moore <dan@moore.cx>
112 * @author Evan Prodromou <evan@status.net>
113 * @author Jeffery To <jeffery.to@gmail.com>
114 * @author Toby Inkster <mail@tobyinkster.co.uk>
115 * @author Zach Copley <zach@status.net>
116 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
117 * @link http://status.net/
119 class ApiAction extends Action
122 const READ_WRITE = 2;
123 public static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
125 public $auth_user = null;
127 public $count = null;
128 public $offset = null;
129 public $limit = null;
130 public $max_id = null;
131 public $since_id = null;
132 public $source = null;
133 public $callback = null;
134 public $format = null; // read (default) or read-write
135 public $access = self::READ_ONLY;
137 public function twitterRelationshipArray($source, $target)
139 $relationship = array();
141 $relationship['source'] =
142 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
143 $relationship['target'] =
144 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
146 return array('relationship' => $relationship);
149 public function relationshipDetailsArray(Profile $source, Profile $target)
153 $details['screen_name'] = $source->getNickname();
154 $details['followed_by'] = $target->isSubscribed($source);
157 $sub = Subscription::getSubscription($source, $target);
158 $details['following'] = true;
159 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
160 } catch (NoResultException $e) {
161 $details['following'] = false;
162 $details['notifications_enabled'] = false;
165 $details['blocking'] = $source->hasBlocked($target);
166 $details['id'] = intval($source->id);
171 public function showTwitterXmlRelationship($relationship)
173 $this->elementStart('relationship');
175 foreach ($relationship as $element => $value) {
176 if ($element == 'source' || $element == 'target') {
177 $this->elementStart($element);
178 $this->showXmlRelationshipDetails($value);
179 $this->elementEnd($element);
183 $this->elementEnd('relationship');
186 public function showXmlRelationshipDetails($details)
188 foreach ($details as $element => $value) {
189 $this->element($element, null, $value);
194 * Overrides XMLOutputter::element to write booleans as strings (true|false).
195 * See that method's documentation for more info.
197 * @param string $tag Element type or tagname
198 * @param array $attrs Array of element attributes, as
200 * @param string $content string content of the element
204 public function element($tag, $attrs = [], $content = "")
206 if (is_bool($content)) {
207 $content = ($content ? 'true' : 'false');
210 return parent::element($tag, $attrs, $content);
213 public function showSingleXmlStatus($notice)
215 $this->initDocument('xml');
216 $twitter_status = $this->twitterStatusArray($notice);
217 $this->showTwitterXmlStatus($twitter_status, 'status', true);
218 $this->endDocument('xml');
221 public function initDocument($type = 'xml')
225 header('Content-Type: application/xml; charset=utf-8');
229 header('Content-Type: application/json; charset=utf-8');
231 // Check for JSONP callback
232 if (isset($this->callback)) {
233 print $this->callback . '(';
237 header("Content-Type: application/rss+xml; charset=utf-8");
238 $this->initTwitterRss();
241 header('Content-Type: application/atom+xml; charset=utf-8');
242 $this->initTwitterAtom();
245 // TRANS: Client error on an API request with an unsupported data format.
246 $this->clientError(_('Not a supported data format.'));
252 public function initTwitterRss()
259 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
260 'xmlns:georss' => 'http://www.georss.org/georss'
263 $this->elementStart('channel');
264 Event::handle('StartApiRss', array($this));
267 public function initTwitterAtom()
270 // FIXME: don't hardcode the language here!
271 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
272 'xml:lang' => 'en-US',
273 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
276 public function twitterStatusArray($notice, $include_user = true)
278 $base = $this->twitterSimpleStatusArray($notice, $include_user);
280 // FIXME: MOVE TO SHARE PLUGIN
281 if (!empty($notice->repeat_of)) {
282 $original = Notice::getKV('id', $notice->repeat_of);
283 if ($original instanceof Notice) {
284 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
285 $base['retweeted_status'] = $orig_array;
292 public function twitterSimpleStatusArray($notice, $include_user = true)
294 $profile = $notice->getProfile();
296 $twitter_status = array();
297 $twitter_status['text'] = $notice->content;
298 $twitter_status['truncated'] = false; # Not possible on StatusNet
299 $twitter_status['created_at'] = self::dateTwitter($notice->created);
301 // We could just do $notice->reply_to but maybe the future holds a
302 // different story for parenting.
303 $parent = $notice->getParent();
304 $in_reply_to = $parent->id;
305 } catch (NoParentNoticeException $e) {
307 } catch (NoResultException $e) {
308 // the in_reply_to message has probably been deleted
311 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
316 $ns = $notice->getSource();
317 if ($ns instanceof Notice_source) {
319 if (!empty($ns->url)) {
320 $source_link = $ns->url;
321 if (!empty($ns->name)) {
327 $twitter_status['uri'] = $notice->getUri();
328 $twitter_status['source'] = $source;
329 $twitter_status['source_link'] = $source_link;
330 $twitter_status['id'] = intval($notice->id);
332 $replier_profile = null;
334 if ($notice->reply_to) {
335 $reply = Notice::getKV(intval($notice->reply_to));
337 $replier_profile = $reply->getProfile();
341 $twitter_status['in_reply_to_user_id'] =
342 ($replier_profile) ? intval($replier_profile->id) : null;
343 $twitter_status['in_reply_to_screen_name'] =
344 ($replier_profile) ? $replier_profile->nickname : null;
347 $notloc = Notice_location::locFromStored($notice);
348 // This is the format that GeoJSON expects stuff to be in
349 $twitter_status['geo'] = array('type' => 'Point',
350 'coordinates' => array((float)$notloc->lat,
351 (float)$notloc->lon));
352 } catch (ServerException $e) {
353 $twitter_status['geo'] = null;
357 $attachments = $notice->attachments();
359 if (!empty($attachments)) {
360 $twitter_status['attachments'] = array();
362 foreach ($attachments as $attachment) {
364 $enclosure_o = $attachment->getEnclosure();
365 $enclosure = array();
366 $enclosure['url'] = $enclosure_o->url;
367 $enclosure['mimetype'] = $enclosure_o->mimetype;
368 $enclosure['size'] = $enclosure_o->size;
369 $twitter_status['attachments'][] = $enclosure;
370 } catch (ServerException $e) {
371 // There was not enough metadata available
376 if ($include_user && $profile) {
377 // Don't get notice (recursive!)
378 $twitter_user = $this->twitterUserArray($profile, false);
379 $twitter_status['user'] = $twitter_user;
382 // StatusNet-specific
384 $twitter_status['statusnet_html'] = $notice->getRendered();
385 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
387 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
388 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
389 array('include_user' => $include_user)));
391 return $twitter_status;
394 public static function dateTwitter($dt)
396 $dateStr = date('d F Y H:i:s', strtotime($dt));
397 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
398 $d->setTimezone(new DateTimeZone(common_timezone()));
399 return $d->format('D M d H:i:s O Y');
402 public function twitterUserArray($profile, $get_notice = false)
404 $twitter_user = array();
407 $user = $profile->getUser();
408 } catch (NoSuchUserException $e) {
412 $twitter_user['id'] = $profile->getID();
413 $twitter_user['name'] = $profile->getBestName();
414 $twitter_user['screen_name'] = $profile->getNickname();
415 $twitter_user['location'] = $profile->location;
416 $twitter_user['description'] = $profile->getDescription();
418 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
419 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
420 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
422 // START introduced by qvitter API, not necessary for StatusNet API
423 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
425 $avatar = Avatar::getUploaded($profile);
426 $origurl = $avatar->displayUrl();
427 } catch (Exception $e) {
428 $origurl = $twitter_user['profile_image_url_profile_size'];
430 $twitter_user['profile_image_url_original'] = $origurl;
432 $twitter_user['groups_count'] = $profile->getGroupCount();
433 foreach (array('linkcolor', 'backgroundcolor') as $key) {
434 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
436 // END introduced by qvitter API, not necessary for StatusNet API
438 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
439 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
440 $twitter_user['followers_count'] = $profile->subscriberCount();
442 // Note: some profiles don't have an associated user
444 $twitter_user['friends_count'] = $profile->subscriptionCount();
446 $twitter_user['created_at'] = self::dateTwitter($profile->created);
450 if (!empty($user) && $user->timezone) {
451 $timezone = $user->timezone;
455 $t->setTimezone(new DateTimeZone($timezone));
457 $twitter_user['utc_offset'] = $t->format('Z');
458 $twitter_user['time_zone'] = $timezone;
459 $twitter_user['statuses_count'] = $profile->noticeCount();
461 // Is the requesting user following this user?
462 // These values might actually also mean "unknown". Ambiguity issues?
463 $twitter_user['following'] = false;
464 $twitter_user['statusnet_blocking'] = false;
465 $twitter_user['notifications'] = false;
467 if ($this->scoped instanceof Profile) {
469 $sub = Subscription::getSubscription($this->scoped, $profile);
471 $twitter_user['following'] = true;
472 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
473 } catch (NoResultException $e) {
474 // well, the values are already false...
476 $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
480 $notice = $profile->getCurrentNotice();
481 if ($notice instanceof Notice) {
483 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
487 // StatusNet-specific
489 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
491 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
492 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
494 return $twitter_user;
497 public function showTwitterXmlStatus($twitter_status, $tag = 'status', $namespaces = false)
501 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
503 $this->elementStart($tag, $attrs);
504 foreach ($twitter_status as $element => $value) {
507 $this->showTwitterXmlUser($twitter_status['user']);
510 $this->element($element, null, common_xml_safe_str($value));
513 $this->showXmlAttachments($twitter_status['attachments']);
516 $this->showGeoXML($value);
518 case 'retweeted_status':
519 // FIXME: MOVE TO SHARE PLUGIN
520 $this->showTwitterXmlStatus($value, 'retweeted_status');
523 if (strncmp($element, 'statusnet_', 10) == 0) {
524 if ($element === 'statusnet_in_groups' && is_array($value)) {
525 // QVITTERFIX because it would cause an array to be sent as $value
526 // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
527 $value = json_encode($value);
529 $this->element('statusnet:' . substr($element, 10), null, $value);
531 $this->element($element, null, $value);
535 $this->elementEnd($tag);
538 public function showTwitterXmlUser($twitter_user, $role = 'user', $namespaces = false)
542 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
544 $this->elementStart($role, $attrs);
545 foreach ($twitter_user as $element => $value) {
546 if ($element == 'status') {
547 $this->showTwitterXmlStatus($twitter_user['status']);
548 } elseif (strncmp($element, 'statusnet_', 10) == 0) {
549 $this->element('statusnet:' . substr($element, 10), null, $value);
551 $this->element($element, null, $value);
554 $this->elementEnd($role);
557 public function showXmlAttachments($attachments)
559 if (!empty($attachments)) {
560 $this->elementStart('attachments', array('type' => 'array'));
561 foreach ($attachments as $attachment) {
563 $attrs['url'] = $attachment['url'];
564 $attrs['mimetype'] = $attachment['mimetype'];
565 $attrs['size'] = $attachment['size'];
566 $this->element('enclosure', $attrs, '');
568 $this->elementEnd('attachments');
572 public function showGeoXML($geo)
576 $this->element('geo');
578 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
579 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
580 $this->elementEnd('geo');
584 public function endDocument($type = 'xml')
591 // Check for JSONP callback
592 if (isset($this->callback)) {
597 $this->endTwitterRss();
600 $this->endTwitterRss();
603 // TRANS: Client error on an API request with an unsupported data format.
604 $this->clientError(_('Not a supported data format.'));
609 public function endTwitterRss()
611 $this->elementEnd('channel');
612 $this->elementEnd('rss');
616 public function showSingleAtomStatus($notice)
618 header('Content-Type: application/atom+xml;type=entry;charset="utf-8"');
619 print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
620 print $notice->asAtomEntry(true, true, true, $this->scoped);
623 public function show_single_json_status($notice)
625 $this->initDocument('json');
626 $status = $this->twitterStatusArray($notice);
627 $this->showJsonObjects($status);
628 $this->endDocument('json');
631 public function showJsonObjects($objects)
633 $json_objects = json_encode($objects);
634 if ($json_objects === false) {
635 $this->clientError(_('JSON encoding failed. Error: ') . json_last_error_msg());
641 public function showXmlTimeline($notice)
643 $this->initDocument('xml');
644 $this->elementStart('statuses', array('type' => 'array',
645 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
647 if (is_array($notice)) {
648 //FIXME: make everything calling showJsonTimeline use only Notice objects
650 foreach ($notice as $n) {
651 $ids[] = $n->getID();
653 $notice = Notice::multiGet('id', $ids);
656 while ($notice->fetch()) {
658 $twitter_status = $this->twitterStatusArray($notice);
659 $this->showTwitterXmlStatus($twitter_status);
660 } catch (Exception $e) {
661 common_log(LOG_ERR, $e->getMessage());
666 $this->elementEnd('statuses');
667 $this->endDocument('xml');
670 public function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
672 $this->initDocument('rss');
674 $this->element('title', null, $title);
675 $this->element('link', null, $link);
677 if (!is_null($self)) {
681 'type' => 'application/rss+xml',
688 if (!is_null($suplink)) {
689 // For FriendFeed's SUP protocol
690 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
691 'rel' => 'http://api.friendfeed.com/2008/03#sup',
693 'type' => 'application/json'));
696 if (!is_null($logo)) {
697 $this->elementStart('image');
698 $this->element('link', null, $link);
699 $this->element('title', null, $title);
700 $this->element('url', null, $logo);
701 $this->elementEnd('image');
704 $this->element('description', null, $subtitle);
705 $this->element('language', null, 'en-us');
706 $this->element('ttl', null, '40');
708 if (is_array($notice)) {
709 //FIXME: make everything calling showJsonTimeline use only Notice objects
711 foreach ($notice as $n) {
712 $ids[] = $n->getID();
714 $notice = Notice::multiGet('id', $ids);
717 while ($notice->fetch()) {
719 $entry = $this->twitterRssEntryArray($notice);
720 $this->showTwitterRssItem($entry);
721 } catch (Exception $e) {
722 common_log(LOG_ERR, $e->getMessage());
723 // continue on exceptions
727 $this->endTwitterRss();
730 public function twitterRssEntryArray($notice)
734 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
735 $profile = $notice->getProfile();
737 // We trim() to avoid extraneous whitespace in the output
739 $entry['content'] = common_xml_safe_str(trim($notice->getRendered()));
740 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
741 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
742 $entry['published'] = common_date_iso8601($notice->created);
744 $taguribase = TagURI::base();
745 $entry['id'] = "tag:$taguribase:$entry[link]";
747 $entry['updated'] = $entry['published'];
748 $entry['author'] = $profile->getBestName();
751 $attachments = $notice->attachments();
752 $enclosures = array();
754 foreach ($attachments as $attachment) {
756 $enclosure_o = $attachment->getEnclosure();
757 $enclosure = array();
758 $enclosure['url'] = $enclosure_o->url;
759 $enclosure['mimetype'] = $enclosure_o->mimetype;
760 $enclosure['size'] = $enclosure_o->size;
761 $enclosures[] = $enclosure;
762 } catch (ServerException $e) {
763 // There was not enough metadata available
767 if (!empty($enclosures)) {
768 $entry['enclosures'] = $enclosures;
772 $tag = new Notice_tag();
773 $tag->notice_id = $notice->id;
775 $entry['tags'] = array();
776 while ($tag->fetch()) {
777 $entry['tags'][] = $tag->tag;
783 $entry['description'] = $entry['content'];
784 $entry['pubDate'] = common_date_rfc2822($notice->created);
785 $entry['guid'] = $entry['link'];
788 $notloc = Notice_location::locFromStored($notice);
789 // This is the format that GeoJSON expects stuff to be in.
790 // showGeoRSS() below uses it for XML output, so we reuse it
791 $entry['geo'] = array('type' => 'Point',
792 'coordinates' => array((float)$notloc->lat,
793 (float)$notloc->lon));
794 } catch (ServerException $e) {
795 $entry['geo'] = null;
798 Event::handle('EndRssEntryArray', array($notice, &$entry));
804 public function showTwitterRssItem($entry)
806 $this->elementStart('item');
807 $this->element('title', null, $entry['title']);
808 $this->element('description', null, $entry['description']);
809 $this->element('pubDate', null, $entry['pubDate']);
810 $this->element('guid', null, $entry['guid']);
811 $this->element('link', null, $entry['link']);
813 // RSS only supports 1 enclosure per item
814 if (array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])) {
815 $enclosure = $entry['enclosures'][0];
816 $this->element('enclosure', array('url' => $enclosure['url'], 'type' => $enclosure['mimetype'], 'length' => $enclosure['size']), null);
819 if (array_key_exists('tags', $entry)) {
820 foreach ($entry['tags'] as $tag) {
821 $this->element('category', null, $tag);
825 $this->showGeoRSS($entry['geo']);
826 $this->elementEnd('item');
829 public function showGeoRSS($geo)
835 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
840 public function showAtomTimeline($notice, $title, $id, $link, $subtitle = null, $suplink = null, $selfuri = null, $logo = null)
842 $this->initDocument('atom');
844 $this->element('title', null, $title);
845 $this->element('id', null, $id);
846 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
848 if (!is_null($logo)) {
849 $this->element('logo', null, $logo);
852 if (!is_null($suplink)) {
853 // For FriendFeed's SUP protocol
854 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
856 'type' => 'application/json'));
859 if (!is_null($selfuri)) {
860 $this->element('link', array('href' => $selfuri,
861 'rel' => 'self', 'type' => 'application/atom+xml'), null);
864 $this->element('updated', null, common_date_iso8601('now'));
865 $this->element('subtitle', null, $subtitle);
867 if (is_array($notice)) {
868 //FIXME: make everything calling showJsonTimeline use only Notice objects
870 foreach ($notice as $n) {
871 $ids[] = $n->getID();
873 $notice = Notice::multiGet('id', $ids);
876 while ($notice->fetch()) {
878 $this->raw($notice->asAtomEntry());
879 } catch (Exception $e) {
880 common_log(LOG_ERR, $e->getMessage());
885 $this->endDocument('atom');
888 public function showRssGroups($group, $title, $link, $subtitle)
890 $this->initDocument('rss');
892 $this->element('title', null, $title);
893 $this->element('link', null, $link);
894 $this->element('description', null, $subtitle);
895 $this->element('language', null, 'en-us');
896 $this->element('ttl', null, '40');
898 if (is_array($group)) {
899 foreach ($group as $g) {
900 $twitter_group = $this->twitterRssGroupArray($g);
901 $this->showTwitterRssItem($twitter_group);
904 while ($group->fetch()) {
905 $twitter_group = $this->twitterRssGroupArray($group);
906 $this->showTwitterRssItem($twitter_group);
910 $this->endTwitterRss();
913 public function twitterRssGroupArray($group)
916 $entry['content'] = $group->description;
917 $entry['title'] = $group->nickname;
918 $entry['link'] = $group->permalink();
919 $entry['published'] = common_date_iso8601($group->created);
920 $entry['updated'] = common_date_iso8601($group->modified);
921 $taguribase = common_config('integration', 'groupuri');
922 $entry['id'] = "group:$taguribase:$entry[link]";
924 $entry['description'] = $entry['content'];
925 $entry['pubDate'] = common_date_rfc2822($group->created);
926 $entry['guid'] = $entry['link'];
931 public function showTwitterAtomEntry($entry)
933 $this->elementStart('entry');
934 $this->element('title', null, common_xml_safe_str($entry['title']));
937 array('type' => 'html'),
938 common_xml_safe_str($entry['content'])
940 $this->element('id', null, $entry['id']);
941 $this->element('published', null, $entry['published']);
942 $this->element('updated', null, $entry['updated']);
943 $this->element('link', array('type' => 'text/html',
944 'href' => $entry['link'],
945 'rel' => 'alternate'));
946 $this->element('link', array('type' => $entry['avatar-type'],
947 'href' => $entry['avatar'],
949 $this->elementStart('author');
951 $this->element('name', null, $entry['author-name']);
952 $this->element('uri', null, $entry['author-uri']);
954 $this->elementEnd('author');
955 $this->elementEnd('entry');
958 public function showAtomGroups($group, $title, $id, $link, $subtitle = null, $selfuri = null)
960 $this->initDocument('atom');
962 $this->element('title', null, common_xml_safe_str($title));
963 $this->element('id', null, $id);
964 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
966 if (!is_null($selfuri)) {
967 $this->element('link', array('href' => $selfuri,
968 'rel' => 'self', 'type' => 'application/atom+xml'), null);
971 $this->element('updated', null, common_date_iso8601('now'));
972 $this->element('subtitle', null, common_xml_safe_str($subtitle));
974 if (is_array($group)) {
975 foreach ($group as $g) {
976 $this->raw($g->asAtomEntry());
979 while ($group->fetch()) {
980 $this->raw($group->asAtomEntry());
984 $this->endDocument('atom');
987 public function showJsonTimeline($notice)
989 $this->initDocument('json');
993 if (is_array($notice)) {
994 //FIXME: make everything calling showJsonTimeline use only Notice objects
996 foreach ($notice as $n) {
997 $ids[] = $n->getID();
999 $notice = Notice::multiGet('id', $ids);
1002 while ($notice->fetch()) {
1004 $twitter_status = $this->twitterStatusArray($notice);
1005 array_push($statuses, $twitter_status);
1006 } catch (Exception $e) {
1007 common_log(LOG_ERR, $e->getMessage());
1012 $this->showJsonObjects($statuses);
1014 $this->endDocument('json');
1017 public function showJsonGroups($group)
1019 $this->initDocument('json');
1023 if (is_array($group)) {
1024 foreach ($group as $g) {
1025 $twitter_group = $this->twitterGroupArray($g);
1026 array_push($groups, $twitter_group);
1029 while ($group->fetch()) {
1030 $twitter_group = $this->twitterGroupArray($group);
1031 array_push($groups, $twitter_group);
1035 $this->showJsonObjects($groups);
1037 $this->endDocument('json');
1040 public function twitterGroupArray($group)
1042 $twitter_group = array();
1044 $twitter_group['id'] = intval($group->id);
1045 $twitter_group['url'] = $group->permalink();
1046 $twitter_group['nickname'] = $group->nickname;
1047 $twitter_group['fullname'] = $group->fullname;
1049 if ($this->scoped instanceof Profile) {
1050 $twitter_group['member'] = $this->scoped->isMember($group);
1051 $twitter_group['blocked'] = Group_block::isBlocked(
1057 $twitter_group['admin_count'] = $group->getAdminCount();
1058 $twitter_group['member_count'] = $group->getMemberCount();
1059 $twitter_group['original_logo'] = $group->original_logo;
1060 $twitter_group['homepage_logo'] = $group->homepage_logo;
1061 $twitter_group['stream_logo'] = $group->stream_logo;
1062 $twitter_group['mini_logo'] = $group->mini_logo;
1063 $twitter_group['homepage'] = $group->homepage;
1064 $twitter_group['description'] = $group->description;
1065 $twitter_group['location'] = $group->location;
1066 $twitter_group['created'] = self::dateTwitter($group->created);
1067 $twitter_group['modified'] = self::dateTwitter($group->modified);
1069 return $twitter_group;
1072 public function showXmlGroups($group)
1074 $this->initDocument('xml');
1075 $this->elementStart('groups', array('type' => 'array'));
1077 if (is_array($group)) {
1078 foreach ($group as $g) {
1079 $twitter_group = $this->twitterGroupArray($g);
1080 $this->showTwitterXmlGroup($twitter_group);
1083 while ($group->fetch()) {
1084 $twitter_group = $this->twitterGroupArray($group);
1085 $this->showTwitterXmlGroup($twitter_group);
1089 $this->elementEnd('groups');
1090 $this->endDocument('xml');
1093 public function showTwitterXmlGroup($twitter_group)
1095 $this->elementStart('group');
1096 foreach ($twitter_group as $element => $value) {
1097 $this->element($element, null, $value);
1099 $this->elementEnd('group');
1102 public function showXmlLists($list, $next_cursor = 0, $prev_cursor = 0)
1104 $this->initDocument('xml');
1105 $this->elementStart('lists_list');
1106 $this->elementStart('lists', array('type' => 'array'));
1108 if (is_array($list)) {
1109 foreach ($list as $l) {
1110 $twitter_list = $this->twitterListArray($l);
1111 $this->showTwitterXmlList($twitter_list);
1114 while ($list->fetch()) {
1115 $twitter_list = $this->twitterListArray($list);
1116 $this->showTwitterXmlList($twitter_list);
1120 $this->elementEnd('lists');
1122 $this->element('next_cursor', null, $next_cursor);
1123 $this->element('previous_cursor', null, $prev_cursor);
1125 $this->elementEnd('lists_list');
1126 $this->endDocument('xml');
1129 public function twitterListArray($list)
1131 $profile = Profile::getKV('id', $list->tagger);
1133 $twitter_list = array();
1134 $twitter_list['id'] = $list->id;
1135 $twitter_list['name'] = $list->tag;
1136 $twitter_list['full_name'] = '@' . $profile->nickname . '/' . $list->tag;;
1137 $twitter_list['slug'] = $list->tag;
1138 $twitter_list['description'] = $list->description;
1139 $twitter_list['subscriber_count'] = $list->subscriberCount();
1140 $twitter_list['member_count'] = $list->taggedCount();
1141 $twitter_list['uri'] = $list->getUri();
1143 if ($this->scoped instanceof Profile) {
1144 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
1146 $twitter_list['following'] = false;
1149 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
1150 $twitter_list['user'] = $this->twitterUserArray($profile, false);
1152 return $twitter_list;
1155 public function showTwitterXmlList($twitter_list)
1157 $this->elementStart('list');
1158 foreach ($twitter_list as $element => $value) {
1159 if ($element == 'user') {
1160 $this->showTwitterXmlUser($value, 'user');
1162 $this->element($element, null, $value);
1165 $this->elementEnd('list');
1168 public function showJsonLists($list, $next_cursor = 0, $prev_cursor = 0)
1170 $this->initDocument('json');
1174 if (is_array($list)) {
1175 foreach ($list as $l) {
1176 $twitter_list = $this->twitterListArray($l);
1177 array_push($lists, $twitter_list);
1180 while ($list->fetch()) {
1181 $twitter_list = $this->twitterListArray($list);
1182 array_push($lists, $twitter_list);
1186 $lists_list = array(
1188 'next_cursor' => $next_cursor,
1189 'next_cursor_str' => strval($next_cursor),
1190 'previous_cursor' => $prev_cursor,
1191 'previous_cursor_str' => strval($prev_cursor)
1194 $this->showJsonObjects($lists_list);
1196 $this->endDocument('json');
1199 public function showTwitterXmlUsers($user)
1201 $this->initDocument('xml');
1202 $this->elementStart('users', array('type' => 'array',
1203 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1205 if (is_array($user)) {
1206 foreach ($user as $u) {
1207 $twitter_user = $this->twitterUserArray($u);
1208 $this->showTwitterXmlUser($twitter_user);
1211 while ($user->fetch()) {
1212 $twitter_user = $this->twitterUserArray($user);
1213 $this->showTwitterXmlUser($twitter_user);
1217 $this->elementEnd('users');
1218 $this->endDocument('xml');
1221 public function showJsonUsers($user)
1223 $this->initDocument('json');
1227 if (is_array($user)) {
1228 foreach ($user as $u) {
1229 $twitter_user = $this->twitterUserArray($u);
1230 array_push($users, $twitter_user);
1233 while ($user->fetch()) {
1234 $twitter_user = $this->twitterUserArray($user);
1235 array_push($users, $twitter_user);
1239 $this->showJsonObjects($users);
1241 $this->endDocument('json');
1244 public function showSingleJsonGroup($group)
1246 $this->initDocument('json');
1247 $twitter_group = $this->twitterGroupArray($group);
1248 $this->showJsonObjects($twitter_group);
1249 $this->endDocument('json');
1252 public function showSingleXmlGroup($group)
1254 $this->initDocument('xml');
1255 $twitter_group = $this->twitterGroupArray($group);
1256 $this->showTwitterXmlGroup($twitter_group);
1257 $this->endDocument('xml');
1260 public function showSingleJsonList($list)
1262 $this->initDocument('json');
1263 $twitter_list = $this->twitterListArray($list);
1264 $this->showJsonObjects($twitter_list);
1265 $this->endDocument('json');
1268 public function showSingleXmlList($list)
1270 $this->initDocument('xml');
1271 $twitter_list = $this->twitterListArray($list);
1272 $this->showTwitterXmlList($twitter_list);
1273 $this->endDocument('xml');
1276 public function endTwitterAtom()
1278 $this->elementEnd('feed');
1282 public function showProfile($profile, $content_type = 'xml', $notice = null, $includeStatuses = true)
1284 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1285 switch ($content_type) {
1287 $this->showTwitterXmlUser($profile_array);
1290 $this->showJsonObjects($profile_array);
1293 // TRANS: Client error on an API request with an unsupported data format.
1294 $this->clientError(_('Not a supported data format.'));
1299 public function getTargetProfile($id)
1303 // Twitter supports these other ways of passing the user ID
1304 if (self::is_decimal($this->arg('id'))) {
1305 return Profile::getKV($this->arg('id'));
1306 } elseif ($this->arg('id')) {
1307 // Screen names currently can only uniquely identify a local user.
1308 $nickname = common_canonical_nickname($this->arg('id'));
1309 $user = User::getKV('nickname', $nickname);
1310 return $user ? $user->getProfile() : null;
1311 } elseif ($this->arg('user_id')) {
1312 // This is to ensure that a non-numeric user_id still
1313 // overrides screen_name even if it doesn't get used
1314 if (self::is_decimal($this->arg('user_id'))) {
1315 return Profile::getKV('id', $this->arg('user_id'));
1317 } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1318 $nickname = common_canonical_nickname($this->arg('screen_name'));
1319 $user = User::getByNickname($nickname);
1320 return $user->getProfile();
1322 // Fall back to trying the currently authenticated user
1323 return $this->scoped;
1325 } elseif (self::is_decimal($id) && intval($id) > 0) {
1326 return Profile::getByID($id);
1328 // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1329 $nickname = common_canonical_nickname($id);
1330 $user = User::getByNickname($nickname);
1331 return $user->getProfile();
1335 private static function is_decimal($str)
1337 return preg_match('/^[0-9]+$/', $str);
1341 * Returns query argument or default value if not found. Certain
1342 * parameters used throughout the API are lightly scrubbed and
1343 * bounds checked. This overrides Action::arg().
1345 * @param string $key requested argument
1346 * @param string $def default value to return if $key is not provided
1350 public function arg($key, $def = null)
1352 // XXX: Do even more input validation/scrubbing?
1354 if (array_key_exists($key, $this->args)) {
1357 $page = (int)$this->args['page'];
1358 return ($page < 1) ? 1 : $page;
1360 $count = (int)$this->args['count'];
1363 } elseif ($count > 200) {
1370 $since_id = (int)$this->args['since_id'];
1371 return ($since_id < 1) ? 0 : $since_id;
1373 $max_id = (int)$this->args['max_id'];
1374 return ($max_id < 1) ? 0 : $max_id;
1376 return parent::arg($key, $def);
1383 public function getTargetGroup($id)
1386 if (self::is_decimal($this->arg('id'))) {
1387 return User_group::getKV('id', $this->arg('id'));
1388 } elseif ($this->arg('id')) {
1389 return User_group::getForNickname($this->arg('id'));
1390 } elseif ($this->arg('group_id')) {
1391 // This is to ensure that a non-numeric group_id still
1392 // overrides group_name even if it doesn't get used
1393 if (self::is_decimal($this->arg('group_id'))) {
1394 return User_group::getKV('id', $this->arg('group_id'));
1396 } elseif ($this->arg('group_name')) {
1397 return User_group::getForNickname($this->arg('group_name'));
1399 } elseif (self::is_decimal($id)) {
1400 return User_group::getKV('id', $id);
1401 } elseif ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1402 return User_group::getKV('uri', urldecode($this->arg('uri')));
1404 return User_group::getForNickname($id);
1408 public function getTargetList($user = null, $id = null)
1410 $tagger = $this->getTargetUser($user);
1414 $id = $this->arg('id');
1418 if (is_numeric($id)) {
1419 $list = Profile_list::getKV('id', $id);
1421 // only if the list with the id belongs to the tagger
1422 if (empty($list) || $list->tagger != $tagger->id) {
1427 $tag = common_canonical_tag($id);
1428 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1431 if (!empty($list) && $list->private) {
1432 if ($this->scoped->id == $list->tagger) {
1442 public function getTargetUser($id)
1445 // Twitter supports these other ways of passing the user ID
1446 if (self::is_decimal($this->arg('id'))) {
1447 return User::getKV($this->arg('id'));
1448 } elseif ($this->arg('id')) {
1449 $nickname = common_canonical_nickname($this->arg('id'));
1450 return User::getKV('nickname', $nickname);
1451 } elseif ($this->arg('user_id')) {
1452 // This is to ensure that a non-numeric user_id still
1453 // overrides screen_name even if it doesn't get used
1454 if (self::is_decimal($this->arg('user_id'))) {
1455 return User::getKV('id', $this->arg('user_id'));
1457 } elseif ($this->arg('screen_name')) {
1458 $nickname = common_canonical_nickname($this->arg('screen_name'));
1459 return User::getKV('nickname', $nickname);
1461 // Fall back to trying the currently authenticated user
1462 return $this->scoped->getUser();
1464 } elseif (self::is_decimal($id)) {
1465 return User::getKV($id);
1467 $nickname = common_canonical_nickname($id);
1468 return User::getKV('nickname', $nickname);
1473 * Calculate the complete URI that called up this action. Used for
1474 * Atom rel="self" links. Warning: this is funky.
1476 * @return string URL a URL suitable for rel="self" Atom links
1478 public function getSelfUri()
1480 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1482 $id = $this->arg('id');
1483 $aargs = array('format' => $this->format);
1488 $user = $this->arg('user');
1489 if (!empty($user)) {
1490 $aargs['user'] = $user;
1493 $tag = $this->arg('tag');
1495 $aargs['tag'] = $tag;
1498 parse_str($_SERVER['QUERY_STRING'], $params);
1500 if (!empty($params)) {
1501 unset($params['p']);
1502 $pstring = http_build_query($params);
1505 $uri = common_local_url($action, $aargs);
1507 if (!empty($pstring)) {
1508 $uri .= '?' . $pstring;
1517 * @param array $args Web and URL arguments
1519 * @return boolean false if user doesn't exist
1521 protected function prepare(array $args = array())
1523 GNUsocial::setApi(true); // reduce exception reports to aid in debugging
1524 parent::prepare($args);
1526 $this->format = $this->arg('format');
1527 $this->callback = $this->arg('callback');
1528 $this->page = (int)$this->arg('page', 1);
1529 $this->count = (int)$this->arg('count', 20);
1530 $this->max_id = (int)$this->arg('max_id', 0);
1531 $this->since_id = (int)$this->arg('since_id', 0);
1533 // These two are not used everywhere, mainly just AtompubAction extensions
1534 $this->offset = ($this->page - 1) * $this->count;
1535 $this->limit = $this->count + 1;
1537 if ($this->arg('since')) {
1538 header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
1541 $this->source = $this->trimmed('source');
1543 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
1544 $this->source = 'api';
1553 * @param array $args Arguments from $_REQUEST
1557 protected function handle()
1559 header('Access-Control-Allow-Origin: *');