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 = ['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)
141 $relationship['source'] =
142 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
143 $relationship['target'] =
144 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
146 return ['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|string|null $attrs Array of element attributes, as key-value pairs
199 * @param string|null $content string content of the element
203 public function element(string $tag, $attrs = null, $content = null)
205 if (is_bool($content)) {
206 $content = ($content ? "true" : "false");
209 parent::element($tag, $attrs, $content);
212 public function showSingleXmlStatus($notice)
214 $this->initDocument('xml');
215 $twitter_status = $this->twitterStatusArray($notice);
216 $this->showTwitterXmlStatus($twitter_status, 'status', true);
217 $this->endDocument('xml');
220 public function initDocument($type = 'xml')
224 header('Content-Type: application/xml; charset=utf-8');
228 header('Content-Type: application/json; charset=utf-8');
230 // Check for JSONP callback
231 if (isset($this->callback)) {
232 print $this->callback . '(';
236 header("Content-Type: application/rss+xml; charset=utf-8");
237 $this->initTwitterRss();
240 header('Content-Type: application/atom+xml; charset=utf-8');
241 $this->initTwitterAtom();
244 // TRANS: Client error on an API request with an unsupported data format.
245 $this->clientError(_('Not a supported data format.'));
251 public function initTwitterRss()
258 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
259 'xmlns:georss' => 'http://www.georss.org/georss'
262 $this->elementStart('channel');
263 Event::handle('StartApiRss', [$this]);
266 public function initTwitterAtom()
269 // FIXME: don't hardcode the language here!
270 $this->elementStart('feed', ['xmlns' => 'http://www.w3.org/2005/Atom',
271 'xml:lang' => 'en-US',
272 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0']);
275 public function twitterStatusArray($notice, $include_user = true)
277 $base = $this->twitterSimpleStatusArray($notice, $include_user);
279 // FIXME: MOVE TO SHARE PLUGIN
280 if (!empty($notice->repeat_of)) {
281 $original = Notice::getKV('id', $notice->repeat_of);
282 if ($original instanceof Notice) {
283 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
284 $base['retweeted_status'] = $orig_array;
291 public function twitterSimpleStatusArray($notice, $include_user = true)
293 $profile = $notice->getProfile();
295 $twitter_status = [];
296 $twitter_status['text'] = $notice->content;
297 $twitter_status['truncated'] = false; # Not possible on StatusNet
298 $twitter_status['created_at'] = self::dateTwitter($notice->created);
300 // We could just do $notice->reply_to but maybe the future holds a
301 // different story for parenting.
302 $parent = $notice->getParent();
303 $in_reply_to = $parent->id;
304 } catch (NoParentNoticeException $e) {
306 } catch (NoResultException $e) {
307 // the in_reply_to message has probably been deleted
310 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
315 $ns = $notice->getSource();
316 if ($ns instanceof Notice_source) {
318 if (!empty($ns->url)) {
319 $source_link = $ns->url;
320 if (!empty($ns->name)) {
326 $twitter_status['uri'] = $notice->getUri();
327 $twitter_status['source'] = $source;
328 $twitter_status['source_link'] = $source_link;
329 $twitter_status['id'] = intval($notice->id);
331 $replier_profile = null;
333 if ($notice->reply_to) {
334 $reply = Notice::getKV(intval($notice->reply_to));
336 $replier_profile = $reply->getProfile();
340 $twitter_status['in_reply_to_user_id'] =
341 ($replier_profile) ? intval($replier_profile->id) : null;
342 $twitter_status['in_reply_to_screen_name'] =
343 ($replier_profile) ? $replier_profile->nickname : null;
346 $notloc = Notice_location::locFromStored($notice);
347 // This is the format that GeoJSON expects stuff to be in
348 $twitter_status['geo'] = ['type' => 'Point',
349 'coordinates' => [(float)$notloc->lat,
350 (float)$notloc->lon]];
351 } catch (ServerException $e) {
352 $twitter_status['geo'] = null;
356 $attachments = $notice->attachments();
358 if (!empty($attachments)) {
359 $twitter_status['attachments'] = [];
361 foreach ($attachments as $attachment) {
363 $enclosure_o = $attachment->getEnclosure();
365 $enclosure['url'] = $enclosure_o->url;
366 $enclosure['mimetype'] = $enclosure_o->mimetype;
367 $enclosure['size'] = $enclosure_o->size;
368 $twitter_status['attachments'][] = $enclosure;
369 } catch (ServerException $e) {
370 // There was not enough metadata available
375 if ($include_user && $profile) {
376 // Don't get notice (recursive!)
377 $twitter_user = $this->twitterUserArray($profile, false);
378 $twitter_status['user'] = $twitter_user;
381 // StatusNet-specific
383 $twitter_status['statusnet_html'] = $notice->getRendered();
384 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
386 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
387 Event::handle('NoticeSimpleStatusArray', [$notice, &$twitter_status, $this->scoped,
388 ['include_user' => $include_user]]);
390 return $twitter_status;
393 public static function dateTwitter($dt)
395 $dateStr = date('d F Y H:i:s', strtotime($dt));
396 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
397 $d->setTimezone(new DateTimeZone(common_timezone()));
398 return $d->format('D M d H:i:s O Y');
401 public function twitterUserArray($profile, $get_notice = false)
406 $user = $profile->getUser();
407 } catch (NoSuchUserException $e) {
411 $twitter_user['id'] = $profile->getID();
412 $twitter_user['name'] = $profile->getBestName();
413 $twitter_user['screen_name'] = $profile->getNickname();
414 $twitter_user['location'] = $profile->location;
415 $twitter_user['description'] = $profile->getDescription();
417 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
418 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
419 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
421 // START introduced by qvitter API, not necessary for StatusNet API
422 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
424 $avatar = Avatar::getUploaded($profile);
425 $origurl = $avatar->displayUrl();
426 } catch (Exception $e) {
427 $origurl = $twitter_user['profile_image_url_profile_size'];
429 $twitter_user['profile_image_url_original'] = $origurl;
431 $twitter_user['groups_count'] = $profile->getGroupCount();
432 foreach (['linkcolor', 'backgroundcolor'] as $key) {
433 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
435 // END introduced by qvitter API, not necessary for StatusNet API
437 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
438 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
439 $twitter_user['followers_count'] = $profile->subscriberCount();
441 // Note: some profiles don't have an associated user
443 $twitter_user['friends_count'] = $profile->subscriptionCount();
445 $twitter_user['created_at'] = self::dateTwitter($profile->created);
449 if (!empty($user) && $user->timezone) {
450 $timezone = $user->timezone;
454 $t->setTimezone(new DateTimeZone($timezone));
456 $twitter_user['utc_offset'] = $t->format('Z');
457 $twitter_user['time_zone'] = $timezone;
458 $twitter_user['statuses_count'] = $profile->noticeCount();
460 // Is the requesting user following this user?
461 // These values might actually also mean "unknown". Ambiguity issues?
462 $twitter_user['following'] = false;
463 $twitter_user['statusnet_blocking'] = false;
464 $twitter_user['notifications'] = false;
466 if ($this->scoped instanceof Profile) {
468 $sub = Subscription::getSubscription($this->scoped, $profile);
470 $twitter_user['following'] = true;
471 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
472 } catch (NoResultException $e) {
473 // well, the values are already false...
475 $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
479 $notice = $profile->getCurrentNotice();
480 if ($notice instanceof Notice) {
482 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
486 // StatusNet-specific
488 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
490 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
491 Event::handle('TwitterUserArray', [$profile, &$twitter_user, $this->scoped, []]);
493 return $twitter_user;
496 public function showTwitterXmlStatus($twitter_status, $tag = 'status', $namespaces = false)
500 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
502 $this->elementStart($tag, $attrs);
503 foreach ($twitter_status as $element => $value) {
506 $this->showTwitterXmlUser($twitter_status['user']);
509 $this->element($element, null, common_xml_safe_str($value));
512 $this->showXmlAttachments($twitter_status['attachments']);
515 $this->showGeoXML($value);
517 case 'retweeted_status':
518 // FIXME: MOVE TO SHARE PLUGIN
519 $this->showTwitterXmlStatus($value, 'retweeted_status');
522 if (strncmp($element, 'statusnet_', 10) == 0) {
523 if ($element === 'statusnet_in_groups' && is_array($value)) {
524 // QVITTERFIX because it would cause an array to be sent as $value
525 // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
526 $value = json_encode($value);
528 $this->element('statusnet:' . substr($element, 10), null, $value);
530 $this->element($element, null, $value);
534 $this->elementEnd($tag);
537 public function showTwitterXmlUser($twitter_user, $role = 'user', $namespaces = false)
541 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
543 $this->elementStart($role, $attrs);
544 foreach ($twitter_user as $element => $value) {
545 if ($element == 'status') {
546 $this->showTwitterXmlStatus($twitter_user['status']);
547 } elseif (strncmp($element, 'statusnet_', 10) == 0) {
548 $this->element('statusnet:' . substr($element, 10), null, $value);
550 $this->element($element, null, $value);
553 $this->elementEnd($role);
556 public function showXmlAttachments($attachments)
558 if (!empty($attachments)) {
559 $this->elementStart('attachments', ['type' => 'array']);
560 foreach ($attachments as $attachment) {
562 $attrs['url'] = $attachment['url'];
563 $attrs['mimetype'] = $attachment['mimetype'];
564 $attrs['size'] = $attachment['size'];
565 $this->element('enclosure', $attrs, '');
567 $this->elementEnd('attachments');
571 public function showGeoXML($geo)
575 $this->element('geo');
577 $this->elementStart('geo', ['xmlns:georss' => 'http://www.georss.org/georss']);
578 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
579 $this->elementEnd('geo');
583 public function endDocument($type = 'xml')
590 // Check for JSONP callback
591 if (isset($this->callback)) {
596 $this->endTwitterRss();
599 $this->endTwitterRss();
602 // TRANS: Client error on an API request with an unsupported data format.
603 $this->clientError(_('Not a supported data format.'));
608 public function endTwitterRss()
610 $this->elementEnd('channel');
611 $this->elementEnd('rss');
615 public function showSingleAtomStatus($notice)
617 header('Content-Type: application/atom+xml;type=entry;charset="utf-8"');
618 print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
619 print $notice->asAtomEntry(true, true, true, $this->scoped);
622 public function show_single_json_status($notice)
624 $this->initDocument('json');
625 $status = $this->twitterStatusArray($notice);
626 $this->showJsonObjects($status);
627 $this->endDocument('json');
630 public function showJsonObjects($objects)
632 $json_objects = json_encode($objects);
633 if ($json_objects === false) {
634 $this->clientError(_('JSON encoding failed. Error: ') . json_last_error_msg());
640 public function showXmlTimeline($notice)
642 $this->initDocument('xml');
643 $this->elementStart('statuses', ['type' => 'array',
644 'xmlns:statusnet' => 'http://status.net/schema/api/1/']);
646 if (is_array($notice)) {
647 //FIXME: make everything calling showJsonTimeline use only Notice objects
649 foreach ($notice as $n) {
650 $ids[] = $n->getID();
652 $notice = Notice::multiGet('id', $ids);
655 while ($notice->fetch()) {
657 $twitter_status = $this->twitterStatusArray($notice);
658 $this->showTwitterXmlStatus($twitter_status);
659 } catch (Exception $e) {
660 common_log(LOG_ERR, $e->getMessage());
665 $this->elementEnd('statuses');
666 $this->endDocument('xml');
669 public function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
671 $this->initDocument('rss');
673 $this->element('title', null, $title);
674 $this->element('link', null, $link);
676 if (!is_null($self)) {
680 'type' => 'application/rss+xml',
687 if (!is_null($suplink)) {
688 // For FriendFeed's SUP protocol
689 $this->element('link', ['xmlns' => 'http://www.w3.org/2005/Atom',
690 'rel' => 'http://api.friendfeed.com/2008/03#sup',
692 'type' => 'application/json']);
695 if (!is_null($logo)) {
696 $this->elementStart('image');
697 $this->element('link', null, $link);
698 $this->element('title', null, $title);
699 $this->element('url', null, $logo);
700 $this->elementEnd('image');
703 $this->element('description', null, $subtitle);
704 $this->element('language', null, 'en-us');
705 $this->element('ttl', null, '40');
707 if (is_array($notice)) {
708 //FIXME: make everything calling showJsonTimeline use only Notice objects
710 foreach ($notice as $n) {
711 $ids[] = $n->getID();
713 $notice = Notice::multiGet('id', $ids);
716 while ($notice->fetch()) {
718 $entry = $this->twitterRssEntryArray($notice);
719 $this->showTwitterRssItem($entry);
720 } catch (Exception $e) {
721 common_log(LOG_ERR, $e->getMessage());
722 // continue on exceptions
726 $this->endTwitterRss();
729 public function twitterRssEntryArray($notice)
733 if (Event::handle('StartRssEntryArray', [$notice, &$entry])) {
734 $profile = $notice->getProfile();
736 // We trim() to avoid extraneous whitespace in the output
738 $entry['content'] = common_xml_safe_str(trim($notice->getRendered()));
739 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
740 $entry['link'] = common_local_url('shownotice', ['notice' => $notice->id]);
741 $entry['published'] = common_date_iso8601($notice->created);
743 $taguribase = TagURI::base();
744 $entry['id'] = "tag:$taguribase:$entry[link]";
746 $entry['updated'] = $entry['published'];
747 $entry['author'] = $profile->getBestName();
750 $attachments = $notice->attachments();
753 foreach ($attachments as $attachment) {
755 $enclosure_o = $attachment->getEnclosure();
757 $enclosure['url'] = $enclosure_o->url;
758 $enclosure['mimetype'] = $enclosure_o->mimetype;
759 $enclosure['size'] = $enclosure_o->size;
760 $enclosures[] = $enclosure;
761 } catch (ServerException $e) {
762 // There was not enough metadata available
766 if (!empty($enclosures)) {
767 $entry['enclosures'] = $enclosures;
771 $tag = new Notice_tag();
772 $tag->notice_id = $notice->id;
775 while ($tag->fetch()) {
776 $entry['tags'][] = $tag->tag;
782 $entry['description'] = $entry['content'];
783 $entry['pubDate'] = common_date_rfc2822($notice->created);
784 $entry['guid'] = $entry['link'];
787 $notloc = Notice_location::locFromStored($notice);
788 // This is the format that GeoJSON expects stuff to be in.
789 // showGeoRSS() below uses it for XML output, so we reuse it
790 $entry['geo'] = ['type' => 'Point',
791 'coordinates' => [(float)$notloc->lat,
792 (float)$notloc->lon]];
793 } catch (ServerException $e) {
794 $entry['geo'] = null;
797 Event::handle('EndRssEntryArray', [$notice, &$entry]);
803 public function showTwitterRssItem($entry)
805 $this->elementStart('item');
806 $this->element('title', null, $entry['title']);
807 $this->element('description', null, $entry['description']);
808 $this->element('pubDate', null, $entry['pubDate']);
809 $this->element('guid', null, $entry['guid']);
810 $this->element('link', null, $entry['link']);
812 // RSS only supports 1 enclosure per item
813 if (array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])) {
814 $enclosure = $entry['enclosures'][0];
815 $this->element('enclosure', ['url' => $enclosure['url'], 'type' => $enclosure['mimetype'], 'length' => $enclosure['size']]);
818 if (array_key_exists('tags', $entry)) {
819 foreach ($entry['tags'] as $tag) {
820 $this->element('category', null, $tag);
824 $this->showGeoRSS($entry['geo']);
825 $this->elementEnd('item');
828 public function showGeoRSS($geo)
834 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
839 public function showAtomTimeline($notice, $title, $id, $link, $subtitle = null, $suplink = null, $selfuri = null, $logo = null)
841 $this->initDocument('atom');
843 $this->element('title', null, $title);
844 $this->element('id', null, $id);
845 $this->element('link', ['href' => $link, 'rel' => 'alternate', 'type' => 'text/html']);
847 if (!is_null($logo)) {
848 $this->element('logo', null, $logo);
851 if (!is_null($suplink)) {
852 // For FriendFeed's SUP protocol
853 $this->element('link', ['rel' => 'http://api.friendfeed.com/2008/03#sup',
855 'type' => 'application/json']);
858 if (!is_null($selfuri)) {
859 $this->element('link', ['href' => $selfuri,
860 'rel' => 'self', 'type' => 'application/atom+xml']);
863 $this->element('updated', null, common_date_iso8601('now'));
864 $this->element('subtitle', null, $subtitle);
866 if (is_array($notice)) {
867 //FIXME: make everything calling showJsonTimeline use only Notice objects
869 foreach ($notice as $n) {
870 $ids[] = $n->getID();
872 $notice = Notice::multiGet('id', $ids);
875 while ($notice->fetch()) {
877 $this->raw($notice->asAtomEntry());
878 } catch (Exception $e) {
879 common_log(LOG_ERR, $e->getMessage());
884 $this->endDocument('atom');
887 public function showRssGroups($group, $title, $link, $subtitle)
889 $this->initDocument('rss');
891 $this->element('title', null, $title);
892 $this->element('link', null, $link);
893 $this->element('description', null, $subtitle);
894 $this->element('language', null, 'en-us');
895 $this->element('ttl', null, '40');
897 if (is_array($group)) {
898 foreach ($group as $g) {
899 $twitter_group = $this->twitterRssGroupArray($g);
900 $this->showTwitterRssItem($twitter_group);
903 while ($group->fetch()) {
904 $twitter_group = $this->twitterRssGroupArray($group);
905 $this->showTwitterRssItem($twitter_group);
909 $this->endTwitterRss();
912 public function twitterRssGroupArray($group)
915 $entry['content'] = $group->description;
916 $entry['title'] = $group->nickname;
917 $entry['link'] = $group->permalink();
918 $entry['published'] = common_date_iso8601($group->created);
919 $entry['updated'] = common_date_iso8601($group->modified);
920 $taguribase = common_config('integration', 'groupuri');
921 $entry['id'] = "group:$taguribase:$entry[link]";
923 $entry['description'] = $entry['content'];
924 $entry['pubDate'] = common_date_rfc2822($group->created);
925 $entry['guid'] = $entry['link'];
930 public function showTwitterAtomEntry($entry)
932 $this->elementStart('entry');
933 $this->element('title', null, common_xml_safe_str($entry['title']));
937 common_xml_safe_str($entry['content'])
939 $this->element('id', null, $entry['id']);
940 $this->element('published', null, $entry['published']);
941 $this->element('updated', null, $entry['updated']);
942 $this->element('link', ['type' => 'text/html',
943 'href' => $entry['link'],
944 'rel' => 'alternate']);
945 $this->element('link', ['type' => $entry['avatar-type'],
946 'href' => $entry['avatar'],
948 $this->elementStart('author');
950 $this->element('name', null, $entry['author-name']);
951 $this->element('uri', null, $entry['author-uri']);
953 $this->elementEnd('author');
954 $this->elementEnd('entry');
957 public function showAtomGroups($group, $title, $id, $link, $subtitle = null, $selfuri = null)
959 $this->initDocument('atom');
961 $this->element('title', null, common_xml_safe_str($title));
962 $this->element('id', null, $id);
963 $this->element('link', ['href' => $link, 'rel' => 'alternate', 'type' => 'text/html']);
965 if (!is_null($selfuri)) {
966 $this->element('link', ['href' => $selfuri,
967 'rel' => 'self', 'type' => 'application/atom+xml']);
970 $this->element('updated', null, common_date_iso8601('now'));
971 $this->element('subtitle', null, common_xml_safe_str($subtitle));
973 if (is_array($group)) {
974 foreach ($group as $g) {
975 $this->raw($g->asAtomEntry());
978 while ($group->fetch()) {
979 $this->raw($group->asAtomEntry());
983 $this->endDocument('atom');
986 public function showJsonTimeline($notice)
988 $this->initDocument('json');
992 if (is_array($notice)) {
993 //FIXME: make everything calling showJsonTimeline use only Notice objects
995 foreach ($notice as $n) {
996 $ids[] = $n->getID();
998 $notice = Notice::multiGet('id', $ids);
1001 while ($notice->fetch()) {
1003 $twitter_status = $this->twitterStatusArray($notice);
1004 array_push($statuses, $twitter_status);
1005 } catch (Exception $e) {
1006 common_log(LOG_ERR, $e->getMessage());
1011 $this->showJsonObjects($statuses);
1013 $this->endDocument('json');
1016 public function showJsonGroups($group)
1018 $this->initDocument('json');
1022 if (is_array($group)) {
1023 foreach ($group as $g) {
1024 $twitter_group = $this->twitterGroupArray($g);
1025 array_push($groups, $twitter_group);
1028 while ($group->fetch()) {
1029 $twitter_group = $this->twitterGroupArray($group);
1030 array_push($groups, $twitter_group);
1034 $this->showJsonObjects($groups);
1036 $this->endDocument('json');
1039 public function twitterGroupArray($group)
1041 $twitter_group = [];
1043 $twitter_group['id'] = intval($group->id);
1044 $twitter_group['url'] = $group->permalink();
1045 $twitter_group['nickname'] = $group->nickname;
1046 $twitter_group['fullname'] = $group->fullname;
1048 if ($this->scoped instanceof Profile) {
1049 $twitter_group['member'] = $this->scoped->isMember($group);
1050 $twitter_group['blocked'] = Group_block::isBlocked(
1056 $twitter_group['admin_count'] = $group->getAdminCount();
1057 $twitter_group['member_count'] = $group->getMemberCount();
1058 $twitter_group['original_logo'] = $group->original_logo;
1059 $twitter_group['homepage_logo'] = $group->homepage_logo;
1060 $twitter_group['stream_logo'] = $group->stream_logo;
1061 $twitter_group['mini_logo'] = $group->mini_logo;
1062 $twitter_group['homepage'] = $group->homepage;
1063 $twitter_group['description'] = $group->description;
1064 $twitter_group['location'] = $group->location;
1065 $twitter_group['created'] = self::dateTwitter($group->created);
1066 $twitter_group['modified'] = self::dateTwitter($group->modified);
1068 return $twitter_group;
1071 public function showXmlGroups($group)
1073 $this->initDocument('xml');
1074 $this->elementStart('groups', ['type' => 'array']);
1076 if (is_array($group)) {
1077 foreach ($group as $g) {
1078 $twitter_group = $this->twitterGroupArray($g);
1079 $this->showTwitterXmlGroup($twitter_group);
1082 while ($group->fetch()) {
1083 $twitter_group = $this->twitterGroupArray($group);
1084 $this->showTwitterXmlGroup($twitter_group);
1088 $this->elementEnd('groups');
1089 $this->endDocument('xml');
1092 public function showTwitterXmlGroup($twitter_group)
1094 $this->elementStart('group');
1095 foreach ($twitter_group as $element => $value) {
1096 $this->element($element, null, $value);
1098 $this->elementEnd('group');
1101 public function showXmlLists($list, $next_cursor = 0, $prev_cursor = 0)
1103 $this->initDocument('xml');
1104 $this->elementStart('lists_list');
1105 $this->elementStart('lists', ['type' => 'array']);
1107 if (is_array($list)) {
1108 foreach ($list as $l) {
1109 $twitter_list = $this->twitterListArray($l);
1110 $this->showTwitterXmlList($twitter_list);
1113 while ($list->fetch()) {
1114 $twitter_list = $this->twitterListArray($list);
1115 $this->showTwitterXmlList($twitter_list);
1119 $this->elementEnd('lists');
1121 $this->element('next_cursor', null, $next_cursor);
1122 $this->element('previous_cursor', null, $prev_cursor);
1124 $this->elementEnd('lists_list');
1125 $this->endDocument('xml');
1128 public function twitterListArray($list)
1130 $profile = Profile::getKV('id', $list->tagger);
1133 $twitter_list['id'] = $list->id;
1134 $twitter_list['name'] = $list->tag;
1135 $twitter_list['full_name'] = '@' . $profile->nickname . '/' . $list->tag;;
1136 $twitter_list['slug'] = $list->tag;
1137 $twitter_list['description'] = $list->description;
1138 $twitter_list['subscriber_count'] = $list->subscriberCount();
1139 $twitter_list['member_count'] = $list->taggedCount();
1140 $twitter_list['uri'] = $list->getUri();
1142 if ($this->scoped instanceof Profile) {
1143 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
1145 $twitter_list['following'] = false;
1148 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
1149 $twitter_list['user'] = $this->twitterUserArray($profile, false);
1151 return $twitter_list;
1154 public function showTwitterXmlList($twitter_list)
1156 $this->elementStart('list');
1157 foreach ($twitter_list as $element => $value) {
1158 if ($element == 'user') {
1159 $this->showTwitterXmlUser($value, 'user');
1161 $this->element($element, null, $value);
1164 $this->elementEnd('list');
1167 public function showJsonLists($list, $next_cursor = 0, $prev_cursor = 0)
1169 $this->initDocument('json');
1173 if (is_array($list)) {
1174 foreach ($list as $l) {
1175 $twitter_list = $this->twitterListArray($l);
1176 array_push($lists, $twitter_list);
1179 while ($list->fetch()) {
1180 $twitter_list = $this->twitterListArray($list);
1181 array_push($lists, $twitter_list);
1187 'next_cursor' => $next_cursor,
1188 'next_cursor_str' => strval($next_cursor),
1189 'previous_cursor' => $prev_cursor,
1190 'previous_cursor_str' => strval($prev_cursor)
1193 $this->showJsonObjects($lists_list);
1195 $this->endDocument('json');
1198 public function showTwitterXmlUsers($user)
1200 $this->initDocument('xml');
1201 $this->elementStart('users', ['type' => 'array',
1202 'xmlns:statusnet' => 'http://status.net/schema/api/1/']);
1204 if (is_array($user)) {
1205 foreach ($user as $u) {
1206 $twitter_user = $this->twitterUserArray($u);
1207 $this->showTwitterXmlUser($twitter_user);
1210 while ($user->fetch()) {
1211 $twitter_user = $this->twitterUserArray($user);
1212 $this->showTwitterXmlUser($twitter_user);
1216 $this->elementEnd('users');
1217 $this->endDocument('xml');
1220 public function showJsonUsers($user)
1222 $this->initDocument('json');
1226 if (is_array($user)) {
1227 foreach ($user as $u) {
1228 $twitter_user = $this->twitterUserArray($u);
1229 array_push($users, $twitter_user);
1232 while ($user->fetch()) {
1233 $twitter_user = $this->twitterUserArray($user);
1234 array_push($users, $twitter_user);
1238 $this->showJsonObjects($users);
1240 $this->endDocument('json');
1243 public function showSingleJsonGroup($group)
1245 $this->initDocument('json');
1246 $twitter_group = $this->twitterGroupArray($group);
1247 $this->showJsonObjects($twitter_group);
1248 $this->endDocument('json');
1251 public function showSingleXmlGroup($group)
1253 $this->initDocument('xml');
1254 $twitter_group = $this->twitterGroupArray($group);
1255 $this->showTwitterXmlGroup($twitter_group);
1256 $this->endDocument('xml');
1259 public function showSingleJsonList($list)
1261 $this->initDocument('json');
1262 $twitter_list = $this->twitterListArray($list);
1263 $this->showJsonObjects($twitter_list);
1264 $this->endDocument('json');
1267 public function showSingleXmlList($list)
1269 $this->initDocument('xml');
1270 $twitter_list = $this->twitterListArray($list);
1271 $this->showTwitterXmlList($twitter_list);
1272 $this->endDocument('xml');
1275 public function endTwitterAtom()
1277 $this->elementEnd('feed');
1281 public function showProfile($profile, $content_type = 'xml', $notice = null, $includeStatuses = true)
1283 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1284 switch ($content_type) {
1286 $this->showTwitterXmlUser($profile_array);
1289 $this->showJsonObjects($profile_array);
1292 // TRANS: Client error on an API request with an unsupported data format.
1293 $this->clientError(_('Not a supported data format.'));
1298 public function getTargetProfile($id)
1301 // Twitter supports these other ways of passing the user ID
1302 if (self::is_decimal($this->arg('id'))) {
1303 return Profile::getKV($this->arg('id'));
1304 } elseif ($this->arg('id')) {
1305 // Screen names currently can only uniquely identify a local user.
1306 $nickname = common_canonical_nickname($this->arg('id'));
1307 $user = User::getKV('nickname', $nickname);
1308 return $user ? $user->getProfile() : null;
1309 } elseif ($this->arg('user_id')) {
1310 // This is to ensure that a non-numeric user_id still
1311 // overrides screen_name even if it doesn't get used
1312 if (self::is_decimal($this->arg('user_id'))) {
1313 return Profile::getKV('id', $this->arg('user_id'));
1315 } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1316 $nickname = common_canonical_nickname($this->arg('screen_name'));
1317 $user = User::getByNickname($nickname);
1318 return $user->getProfile();
1320 // Fall back to trying the currently authenticated user
1321 return $this->scoped;
1325 if (self::is_decimal($id) && intval($id) > 0) {
1326 return Profile::getByID($id);
1329 // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1330 $nickname = common_canonical_nickname($id);
1331 $user = User::getByNickname($nickname);
1332 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'));
1401 if (self::is_decimal($id)) {
1402 return User_group::getKV('id', $id);
1403 } elseif ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1404 return User_group::getKV('uri', urldecode($this->arg('uri')));
1407 return User_group::getForNickname($id);
1410 public function getTargetList($user = null, $id = null)
1412 $tagger = $this->getTargetUser($user);
1416 $id = $this->arg('id');
1420 if (is_numeric($id)) {
1421 $list = Profile_list::getKV('id', $id);
1423 // only if the list with the id belongs to the tagger
1424 if (empty($list) || $list->tagger != $tagger->id) {
1429 $tag = common_canonical_tag($id);
1430 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1433 if (!empty($list) && $list->private) {
1434 if ($this->scoped->id == $list->tagger) {
1444 public function getTargetUser($id)
1447 // Twitter supports these other ways of passing the user ID
1448 if (self::is_decimal($this->arg('id'))) {
1449 return User::getKV($this->arg('id'));
1450 } elseif ($this->arg('id')) {
1451 $nickname = common_canonical_nickname($this->arg('id'));
1452 return User::getKV('nickname', $nickname);
1453 } elseif ($this->arg('user_id')) {
1454 // This is to ensure that a non-numeric user_id still
1455 // overrides screen_name even if it doesn't get used
1456 if (self::is_decimal($this->arg('user_id'))) {
1457 return User::getKV('id', $this->arg('user_id'));
1459 } elseif ($this->arg('screen_name')) {
1460 $nickname = common_canonical_nickname($this->arg('screen_name'));
1461 return User::getKV('nickname', $nickname);
1463 // Fall back to trying the currently authenticated user
1464 return $this->scoped->getUser();
1468 if (self::is_decimal($id)) {
1469 return User::getKV($id);
1472 $nickname = common_canonical_nickname($id);
1473 return User::getKV('nickname', $nickname);
1477 * Calculate the complete URI that called up this action. Used for
1478 * Atom rel="self" links. Warning: this is funky.
1480 * @return string URL a URL suitable for rel="self" Atom links
1482 public function getSelfUri()
1484 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1486 $id = $this->arg('id');
1487 $aargs = ['format' => $this->format];
1492 $user = $this->arg('user');
1493 if (!empty($user)) {
1494 $aargs['user'] = $user;
1497 $tag = $this->arg('tag');
1499 $aargs['tag'] = $tag;
1502 parse_str($_SERVER['QUERY_STRING'], $params);
1504 if (!empty($params)) {
1505 unset($params['p']);
1506 $pstring = http_build_query($params);
1509 $uri = common_local_url($action, $aargs);
1511 if (!empty($pstring)) {
1512 $uri .= '?' . $pstring;
1521 * @param array $args Web and URL arguments
1523 * @return boolean false if user doesn't exist
1524 * @throws ClientException
1526 protected function prepare(array $args = [])
1528 GNUsocial::setApi(true); // reduce exception reports to aid in debugging
1529 parent::prepare($args);
1531 $this->format = $this->arg('format');
1532 $this->callback = $this->arg('callback');
1533 $this->page = (int)$this->arg('page', 1);
1534 $this->count = (int)$this->arg('count', 20);
1535 $this->max_id = (int)$this->arg('max_id', 0);
1536 $this->since_id = (int)$this->arg('since_id', 0);
1538 // These two are not used everywhere, mainly just AtompubAction extensions
1539 $this->offset = ($this->page - 1) * $this->count;
1540 $this->limit = $this->count + 1;
1542 if ($this->arg('since')) {
1543 header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
1546 $this->source = $this->trimmed('source');
1548 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
1549 $this->source = 'api';
1560 protected function handle()
1562 header('Access-Control-Allow-Origin: *');