diff --git a/.gitignore b/.gitignore index 3cbb1f8f6c..2027e9ffa6 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ plugin/YPTSocket/AVideo-Socket/ .phpunit.result.cache phpunit.xml +plugin/API/AVideo-NodeAPIBoost/ diff --git a/.htaccess b/.htaccess index e3c53e7b92..f4eb560104 100644 --- a/.htaccess +++ b/.htaccess @@ -80,7 +80,6 @@ Options All -Indexes - # END Caching RewriteCond %{HTTPS} =on @@ -91,6 +90,9 @@ Options All -Indexes RewriteEngine on #VideoHLS for DRM + RewriteRule ^api/([^/]+)/([^/]+)$ plugin/API/router.php?APIPlugin=$1&APIName=$2 [QSA,L] + RewriteRule ^api/([^/]+)$ plugin/API/router.php?APIName=$1 [QSA,L] + RewriteRule ^buy/? plugin/YPTWallet/buy.php [NC,L,QSA] RewriteRule ^id/? view/id.php [NC,L,QSA] RewriteRule ^logo.png view/logo.png.php [NC,L,QSA] diff --git a/composer.json b/composer.json index 68bcbdacf4..f11645301c 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,8 @@ "react/event-loop": "^1.5", "elephantio/elephant.io": "^4.13", "iamcal/sql-parser": "^0.6", - "ratchet/pawl": "^0.4.3" + "ratchet/pawl": "^0.4.3", + "zircote/swagger-php": "^5.0" }, "require-dev": { "phpunit/phpunit": "^9.6", diff --git a/composer.lock b/composer.lock index b5c6e7404e..9dde446c55 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84e9f6a11f1bf0940cf44e61d9477687", + "content-hash": "b796d698ec634bf301a3ec925a25578d", "packages": [ { "name": "abraham/twitteroauth", @@ -2396,6 +2396,64 @@ ], "time": "2025-01-08T20:10:23+00:00" }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, { "name": "norkunas/onesignal-php-api", "version": "v2.15.0", @@ -4838,6 +4896,69 @@ ], "time": "2024-09-25T14:11:13+00:00" }, + { + "name": "symfony/finder", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "63741784cd7b9967975eec610b256eed3ede022b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-28T13:32:08+00:00" + }, { "name": "symfony/http-client", "version": "v5.4.49", @@ -6214,6 +6335,81 @@ ], "time": "2024-09-25T14:11:13+00:00" }, + { + "name": "symfony/yaml", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a454d47278cc16a5db371fe73ae66a78a633371e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e", + "reference": "a454d47278cc16a5db371fe73ae66a78a633371e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.3" + }, + "require-dev": { + "symfony/console": "^5.3|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, { "name": "thecodingmachine/safe", "version": "v2.5.0", @@ -6405,6 +6601,92 @@ "source": "https://github.com/vimeo/vimeo.php/tree/3.0.10" }, "time": "2022-02-18T14:05:55+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "18457fa71f753cfd4a2b21916baf329864fdfaa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/18457fa71f753cfd4a2b21916baf329864fdfaa6", + "reference": "18457fa71f753cfd4a2b21916baf329864fdfaa6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/php-parser": "^4.19 || ^5.0", + "php": ">=7.4", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": "^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^5.0 || ^6.0 || ^7.0" + }, + "conflict": { + "symfony/process": ">=6, <6.4.14" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.62.0", + "phpstan/phpstan": "^1.6 || ^2.0", + "phpunit/phpunit": "^9.0", + "rector/rector": "^1.0 || ^2.0", + "vimeo/psalm": "^4.30 || ^5.0" + }, + "suggest": { + "doctrine/annotations": "^2.0" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/5.0.7" + }, + "time": "2025-03-19T03:31:11+00:00" } ], "packages-dev": [ @@ -6672,64 +6954,6 @@ ], "time": "2025-02-12T12:17:51+00:00" }, - { - "name": "nikic/php-parser", - "version": "v5.4.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" - }, - "time": "2024-12-30T11:07:19+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", diff --git a/objects/functions.php b/objects/functions.php index 2e1b1dc7f4..6c5ceb0f2b 100644 --- a/objects/functions.php +++ b/objects/functions.php @@ -2579,8 +2579,8 @@ function allowOrigin() header('Access-Control-Allow-Private-Network: true'); header('Access-Control-Request-Private-Network: true'); //header("Access-Control-Allow-Credentials: true"); - header("Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT"); - header("Access-Control-Allow-Headers: Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"); + header("Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,PUT,DELETE"); + header("Access-Control-Allow-Headers: Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers,ua-resolution,Authorization"); } function cleanUpAccessControlHeader() diff --git a/objects/functionsDocker.php b/objects/functionsDocker.php index cac3d1153f..282067ca58 100644 --- a/objects/functionsDocker.php +++ b/objects/functionsDocker.php @@ -3,6 +3,9 @@ function getDockerVarsFileName() { global $global; + if(empty($global['docker_vars'])){ + return false; + } return $global['docker_vars']; } @@ -33,4 +36,4 @@ function getDockerInternalURL() function getDockerStatsURL() { return getDockerInternalURL() . "stat"; -} \ No newline at end of file +} diff --git a/objects/functionsPHP.php b/objects/functionsPHP.php index 96d165f4ea..da249a978f 100644 --- a/objects/functionsPHP.php +++ b/objects/functionsPHP.php @@ -92,8 +92,8 @@ function _ob_get_clean() function _setcookie($cookieName, $value, $expires = 0) { - if($cookieName === 'pass'){ - _error_log('_setcookie pass changed '.$value); + if ($cookieName === 'pass') { + _error_log('_setcookie pass changed ' . $value); } global $config, $global; if (empty($expires)) { @@ -124,7 +124,7 @@ function _setcookie($cookieName, $value, $expires = 0) setcookie($cookieName, $value, (int) $expires, "/", $domain); setcookie($cookieName, $value, (int) $expires, "/", 'www.' . $domain); } - $_COOKIE[$cookieName]=$value; + $_COOKIE[$cookieName] = $value; } function _unsetcookie($cookieName) @@ -246,20 +246,22 @@ function _json_decode($object, $associative = false) } } -function _session_write_close(){ +function _session_write_close() +{ if (isSessionStarted()) { //_error_log(json_encode(debug_backtrace())); @session_write_close(); } } -function isSessionStarted() { - global $customSessionHandle; +function isSessionStarted() +{ + global $customSessionHandle; - if(session_status() == PHP_SESSION_NONE){ + if (session_status() == PHP_SESSION_NONE) { return false; } - if(session_status() == PHP_SESSION_ACTIVE){ + if (session_status() == PHP_SESSION_ACTIVE) { return true; } // Check if a session variable exists in Memcached @@ -270,22 +272,23 @@ function isSessionStarted() { } } -function session_start_preload(){ +function session_start_preload() +{ global $_session_start_preload, $global; - if(empty($global['systemRootPath'])){ + if (empty($global['systemRootPath'])) { return false; } - if(!class_exists('AVideoConf')){ + if (!class_exists('AVideoConf')) { require $global['systemRootPath'] . 'objects/configuration.php'; } - if(!isset($_session_start_preload)){ + if (!isset($_session_start_preload)) { $_session_start_preload = 1; - }else{ + } else { return false; } $config = new AVideoConf(); - + // server should keep session data for AT LEAST 1 hour ini_set('session.gc_maxlifetime', $config->getSession_timeout()); @@ -311,40 +314,40 @@ function _session_start(array $options = []) { try { session_start_preload(); - + // Start session first, then check the session ID if (isset($_GET['PHPSESSID']) && !_empty($_GET['PHPSESSID'])) { $PHPSESSID = $_GET['PHPSESSID']; unset($_GET['PHPSESSID']); - + // Start the session with the options $session = @session_start($options); - + // Now, check if session ID matches after session start if ($PHPSESSID === session_id()) { // Session ID already matches, do nothing return $session; } - + if (!User::isLogged()) { if ($PHPSESSID !== session_id()) { _session_write_close(); session_id($PHPSESSID); //_error_log("captcha: session_id changed to {$PHPSESSID}"); } - + // Restart session after changing the session ID $session = @session_start($options); - + if (preg_match('/objects\/getCaptcha\.php/i', $_SERVER['SCRIPT_NAME'])) { $regenerateSessionId = false; } - + if (!blackListRegenerateSession()) { _error_log("captcha: session_id regenerated new session_id=" . session_id()); _session_regenerate_id(); } - + return $session; } else { //_error_log("captcha: user logged we will not change the session ID PHPSESSID={$PHPSESSID} session_id=" . session_id()); @@ -419,3 +422,57 @@ function doesPHPVersioHasOBBug() { return (version_compare(phpversion(), '8.1.4', '==') || version_compare(phpversion(), '8.0.17', '==')); } + + +function getSystemAPIs() +{ + global $global; + $obj = AVideoPlugin::getObjectData("API"); + $methodsList = array(); + + $reflector = new ReflectionClass('API'); + $class_methods = get_class_methods('API'); + foreach ($class_methods as $key => $met) { + if (preg_match("/(get|set)_api_(.*)/", $met, $matches)) { + $methodsList[] = array($met, $reflector, $matches[1], $matches[2], 'API'); + } + } + + $plugins = Plugin::getAllEnabled(); + foreach ($plugins as $value) { + $p = AVideoPlugin::loadPlugin($value['dirName']); + if (class_exists($value['dirName'])) { + $class_methods = get_class_methods($value['dirName']); + $reflector = new ReflectionClass($value['dirName']); + foreach ($class_methods as $key => $met) { + if (preg_match("/API_(get|set)_(.*)/", $met, $matches)) { + $methodsList[] = array($met, $reflector, $matches[1], $matches[2], $value['dirName']); + } + } + } + } + + $response = array(); + $plugins = array(); + foreach ($methodsList as $method) { + if (!preg_match("/(get|set)_api_(.*)/", $method[0], $matches)) { + if (!preg_match("/API_(get|set)_(.*)/", $method[0], $matches)) { + continue; + } + } + $reflector = $method[1]; + $comment = $reflector->getMethod($method[0])->getDocComment(); + $comment = str_replace(['{webSiteRootURL}', '{getOrSet}', '{APIPlugin}', '{APIName}', '{APISecret}'], [$global['webSiteRootURL'], $method[2], $method[4], $method[3], $obj->APISecret], $comment); + $resp = array( + 'comment' => $comment, + 'method' => $method[0], + 'type' => $method[2], + 'action' => $method[3], + 'plugin' => $method[4], + ); + $plugins[$method[4]][$method[0]] = $resp ; + $response[] = $resp ; + } + return array('methodsList' => $methodsList, 'response' => $response, 'plugins' => $plugins); +} + diff --git a/objects/functionsSecurity.php b/objects/functionsSecurity.php index 4b03020eaf..e8f539bffb 100644 --- a/objects/functionsSecurity.php +++ b/objects/functionsSecurity.php @@ -535,3 +535,49 @@ function escapeshellcmdURL(string $command) function recreateCache(){ return (!empty($_REQUEST['recreate']) && !isBot()); } + +function getBearerToken() +{ + $headers = []; + + // 1. Try apache_request_headers() if available + if (function_exists('apache_request_headers')) { + $headers = apache_request_headers(); + } + + // 2. If still empty, try getallheaders() + if (empty($headers) && function_exists('getallheaders')) { + $headers = getallheaders(); + } + + // 3. If still empty, manually build headers from $_SERVER + if (empty($headers)) { + foreach ($_SERVER as $key => $value) { + if (stripos($key, 'HTTP_') === 0) { + // Convert HTTP_HEADER_NAME to Header-Name + $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + $headers[$headerName] = $value; + } + } + } + + // 4. Normalize and extract Authorization header + foreach ($headers as $name => $value) { + if (strcasecmp($name, 'Authorization') === 0 && preg_match('/Bearer\s(\S+)/', $value, $matches)) { + return $matches[1]; // Return the token + } + } + + // 5. Final fallback: check $_SERVER directly + if (isset($_SERVER['HTTP_AUTHORIZATION']) && preg_match('/Bearer\s(\S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matches)) { + return $matches[1]; + } + + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && preg_match('/Bearer\s(\S+)/', $_SERVER['REDIRECT_HTTP_AUTHORIZATION'], $matches)) { + return $matches[1]; + } + + return null; // Token not found +} + + diff --git a/objects/plugin.php b/objects/plugin.php index 77228aa90a..56d746292d 100644 --- a/objects/plugin.php +++ b/objects/plugin.php @@ -1,11 +1,11 @@ current)) { + $obj->current = getCurrentPage(); + } + if (!isset($obj->rowCount)) { + $obj->rowCount = getRowCount(); + } + + $obj->hasMore = true; + if (!empty($obj->rows) && is_array($obj->rows)) { + if (count($obj->rows) < $obj->rowCount) { + $obj->hasMore = false; + } + } else if (!empty($obj->videos) && is_array($obj->videos)) { + if (count($obj->videos) < $obj->rowCount) { + $obj->hasMore = false; + } + } + if ($obj->current * $obj->rowCount >= $obj->totalRows) { + $obj->hasMore = false; + } + return $obj; + } + + public function getEmptyDataObject() + { + global $global; + $obj = new stdClass(); + $obj->APISecret = md5($global['salt'] . $global['systemRootPath'] . 'API'); + $obj->standAloneFFMPEG = ''; + return $obj; + } + + public function getPluginMenu() + { + global $global; + $fileAPIName = $global['systemRootPath'] . 'plugin/API/pluginMenu.html'; + return file_get_contents($fileAPIName); + } + + /** + * @OA\Post( + * path="/api/set", + * summary="Generic API setter", + * tags={"API"}, + * @OA\Parameter( + * name="APIName", + * in="query", + * required=true, + * @OA\Schema(type="string"), + * description="The name of the API to call" + * ), + * @OA\Parameter( + * name="user", + * in="query", + * required=false, + * @OA\Schema(type="string"), + * description="Username to authenticate" + * ), + * @OA\Parameter( + * name="password", + * in="query", + * required=false, + * @OA\Schema(type="string"), + * description="Password for the user" + * ), + * @OA\Parameter( + * name="encodedPass", + * in="query", + * required=false, + * @OA\Schema(type="boolean"), + * description="Whether the password is encoded" + * ), + * @OA\Response( + * response=200, + * description="API call result", + * @OA\JsonContent(ref="#/components/schemas/ApiObject") + * ) + * ) + */ + public function set($parameters) + { + if (empty($parameters['APIName'])) { + $object = new ApiObject("Parameter APIName can not be empty (set)"); + } else { + if (!empty($parameters['pass'])) { + $parameters['password'] = $parameters['pass']; + } + if (!empty($parameters['encodedPass']) && strtolower($parameters['encodedPass']) === 'false') { + $parameters['encodedPass'] = false; + } + if (!empty($parameters['user']) && !empty($parameters['password'])) { + $user = new User("", $parameters['user'], $parameters['password']); + $user->login(false, @$parameters['encodedPass']); + } + $APIName = $parameters['APIName']; + if (method_exists($this, "set_api_$APIName")) { + $str = "\$object = \$this->set_api_$APIName(\$parameters);"; + eval($str); + } else { + $method = "API_set_{$parameters['APIName']}"; + if ( + !empty($parameters['APIPlugin']) && + AVideoPlugin::isEnabledByName($parameters['APIPlugin']) && + method_exists($parameters['APIPlugin'], $method) + ) { + $str = "\$object = {$parameters['APIPlugin']}::{$method}(\$parameters);"; + eval($str); + } else { + $object = new ApiObject(); + } + } + } + return $object; + } + + public function get($parameters) + { + if (empty($parameters['APIName'])) { + $object = new ApiObject("Parameter APIName can not be empty (get)"); + } else { + if (!empty($parameters['pass'])) { + $parameters['password'] = $parameters['pass']; + } + if (!empty($parameters['user']) && !empty($parameters['password'])) { + if (!empty($parameters['encodedPass']) && strtolower($parameters['encodedPass']) === 'false') { + $parameters['encodedPass'] = false; + } + $user = new User("", $parameters['user'], $parameters['password']); + $user->login(false, @$parameters['encodedPass']); + } + $APIName = $parameters['APIName']; + if (method_exists($this, "get_api_$APIName")) { + $str = "\$object = \$this->get_api_$APIName(\$parameters);"; + eval($str); + } else { + $method = "API_get_{$parameters['APIName']}"; + if ( + !empty($parameters['APIPlugin']) && + AVideoPlugin::isEnabledByName($parameters['APIPlugin']) && + method_exists($parameters['APIPlugin'], $method) + ) { + $str = "\$object = {$parameters['APIPlugin']}::{$method}(\$parameters);"; + eval($str); + } else { + $object = new ApiObject(); + } + } + } + return $object; + } + + private function startResponseObject($parameters) + { + $obj = new stdClass(); + if (empty($parameters['sort']) && !empty($parameters['order'][0]['dir'])) { + $index = intval($parameters['order'][0]['column']); + $parameters['sort'][$parameters['columns'][$index]['data']] = $_GET['order'][0]['dir']; + } + $array = ['sort', 'rowCount', 'current', 'searchPhrase']; + foreach ($array as $value) { + if (!empty($parameters[$value])) { + $obj->$value = $parameters[$value]; + $_POST[$value] = $parameters[$value]; + } + } + + return $obj; + } + + private function getToPost() + { + foreach ($_GET as $key => $value) { + $_POST[$key] = $value; + } + } + + /** + * Retrieves the configuration parameters of a given plugin. + * + * @param array $parameters { + * @type string $plugin_name Required. The name of the plugin to retrieve the parameters for. + * @type string $APISecret Required. A valid API secret to authorize the request. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&rowCount=3&APISecret={APISecret} + * + * @return \ApiObject + */ + /** + * @OA\Get( + * path="/api/plugin_parameters", + * summary="Get Plugin parameters", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_plugin_parameters($parameters) + { + global $global; + $name = "get_api_plugin_parameters" . json_encode($parameters); + $obj = ObjectYPT::getCacheGlobal($name, 3600); + if (empty($obj)) { + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['plugin_name'])) { + if (self::isAPISecretValid()) { + $obj->response = AVideoPlugin::getDataObject($parameters['plugin_name']); + } else { + return new ApiObject("APISecret is required"); + } + } else { + return new ApiObject("Plugin name Not found"); + } + ObjectYPT::setCacheGlobal($name, $obj); + } + return new ApiObject("", false, $obj); + } + + /** + * Get ads information for a specific video, user, or live stream. + * + * @param array $parameters { + * @type int|null $users_id Optional. The user ID to retrieve specific ads. Falls back to global ads if none found. + * @type int|null $videos_id Optional. If provided, the owner of the video will be used to retrieve ads. + * @type string|null $live_key Optional. If provided and the Live plugin is enabled, the user ID will be resolved by the live stream key. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1 + * + * @return array|\ApiObject An array with ads data or an ApiObject containing an error message. + */ + /** + * @OA\Get( + * path="/api/adsInfo", + * summary="Get Adsinfo", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_adsInfo($parameters) + { + + $ads = AVideoPlugin::loadPluginIfEnabled('ADs'); + if (empty($ads)) { + return new ApiObject("ADs Plugin is disabled"); + } + $users_id = 0; + if (!empty($parameters['users_id'])) { + $users_id = $parameters['users_id']; + } else if (!empty($parameters['videos_id'])) { + $users_id = Video::getOwner($parameters['videos_id']); + } else if (!empty($parameters['live_key']) && AVideoPlugin::isEnabledByName('Live')) { + $row = LiveTransmition::keyExists($parameters['live_key']); + $users_id = $row['users_id']; + } + $ad = AVideoPlugin::getObjectDataIfEnabled('ADs'); + $array = array('users_id' => $users_id, 'ads' => array()); + foreach (ADs::AdsPositions as $key => $value) { + $type = $value[0]; + $desktopGlobal = false; + $mobileGlobal = false; + $desktop = ADs::getAds($type, $users_id); + if (empty($desktop)) { + $desktopGlobal = true; + $desktop = ADs::getAds($type, false); + } + $mobile = ADs::getAds($type . 'Mobile', $users_id); + if (empty($mobile)) { + $mobileGlobal = true; + $mobile = ADs::getAds($type . 'Mobile', false); + } + //var_dump($desktop);exit; + $desktopURLs = array(); + foreach ($desktop as $item) { + $desktopURLs[] = array('image' => $item['imageURL'], 'url' => $item['url'], 'info' => $item['txt']); + } + + $mobileURLs = array(); + foreach ($mobile as $item) { + $mobileURLs[] = array('image' => $item['imageURL'], 'url' => $item['url'], 'info' => $item['txt']); + } + $label = ''; + eval("\$label = \$ad->{$type}Label;"); + $array['ads'][] = array( + 'label' => $label, + 'type' => $type, + 'desktop' => array('isValid' => !empty($desktopURLs), 'isGlobal' => $desktopGlobal, 'urls' => $desktopURLs), + 'mobile' => array('isValid' => !empty($mobileURLs), 'isGlobal' => $mobileGlobal, 'urls' => $mobileURLs) + ); + } + + return new ApiObject("", false, $array); + } + + + /** + * Detects if the request is coming from a mobile device based on User-Agent and headers. + * + * @param array $parameters { + * @type string $userAgent Required. Typically the value of $_SERVER["HTTP_USER_AGENT"]. + * @type string|array $httpHeaders Optional. Typically the value of getallheaders() or $_SERVER["HTTP_X_REQUESTED_WITH"]. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=Mozilla%2F5.0+... + * + * @return \ApiObject Object with detection result and debug information. + */ + /** + * @OA\Get( + * path="/api/id", + * summary="Get Id", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_id($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + $obj->id = getPlatformId(); + $obj->isAPISecretValid = self::isAPISecretValid(); + + return new ApiObject("", false, $obj); + } + + /** + * Checks if the request is coming from a mobile device based on User-Agent and HTTP headers. + * + * @param array $parameters { + * @type string $userAgent Required. Typically the value of $_SERVER["HTTP_USER_AGENT"]. + * @type string|array|null $httpHeaders Optional. Usually the value of json_encoded getallheaders() or $_SERVER["HTTP_X_REQUESTED_WITH"]. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=Mozilla%2F5.0... + * + * @return \ApiObject Object containing `userAgent`, `httpHeaders`, and a boolean `isMobile` flag. + */ + /** + * @OA\Get( + * path="/api/is_mobile", + * summary="Get Is mobile", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_is_mobile($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + if (!empty($_REQUEST['httpHeaders'])) { + $json = _json_decode($_REQUEST['httpHeaders']); + if (!empty($json)) { + $_REQUEST['httpHeaders'] = $json; + } else { + $_REQUEST['httpHeaders'] = [$_REQUEST['httpHeaders']]; + } + } + $obj->userAgent = @$_REQUEST['userAgent']; + $obj->httpHeaders = @$_REQUEST['httpHeaders']; + $obj->isMobile = isMobile(@$obj->userAgent, @$obj->httpHeaders); + return new ApiObject("", false, $obj); + } + + /** + * Retrieves categories with optional filters such as pagination, search, and specific category name. + * + * @param array $parameters { + * @type array $sort Optional. Sorting options, e.g., ['created' => 'DESC']. + * @type int|null $rowCount Optional. Maximum number of rows to return. + * @type int|null $current Optional. Current page number. + * @type string|null $searchPhrase Optional. Search keyword to filter categories. + * @type bool|null $parentsOnly Optional. If true, only parent categories will be returned. + * @type string|null $catName Optional. The `clean_name` of a specific category to retrieve. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&rowCount=3¤t=1&sort[created]=DESC + * + * @return \ApiObject Object containing filtered categories, metadata, and image information. + */ + + /** + * @OA\Get( + * path="/api/category", + * summary="Get Category", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_category($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/category.php'; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['catName'])) { + $row = Category::getCategoryByName($parameters['catName']); + $fullTotals = Category::getTotalFromCategory($row['id'], false, true, true); + $totals = Category::getTotalFromCategory($row['id']); + $row['total'] = $totals['total']; + $row['fullTotal'] = $fullTotals['total']; + $row['fullTotal_videos'] = $fullTotals['videos']; + $row['fullTotal_lives'] = $fullTotals['lives']; + $row['fullTotal_livelinks'] = $fullTotals['livelinks']; + $row['fullTotal_livelinks'] = $fullTotals['livelinks']; + + $rows = array($row); + } else { + $rows = Category::getAllCategories(); + } + foreach ($rows as $key => $value) { + $totalVideosOnChilds = Category::getTotalFromChildCategory($value['id']); + $childs = Category::getChildCategories($value['id']); + $photo = Category::getCategoryPhotoPath($value['id']); + $photoBg = Category::getCategoryBackgroundPath($value['id']); + $link = $global['webSiteRootURL'] . 'cat/' . $value['clean_name']; + + if (!empty($value['fullTotal_videos'])) { + $video = Category::getLatestVideoFromCategory($value['id'], true, true); + $images = Video::getImageFromID($video['id']); + $image = $images->default['url']; + } elseif (!empty($value['fullTotal_lives'])) { + $live = Category::getLatestLiveFromCategory($value['id'], true, true); + $image = Live::getImage($live['users_id'], $live['live_servers_id']); + } elseif (!empty($value['fullTotal_livelinks'])) { + $liveLinks = Category::getLatestLiveLinksFromCategory($value['id'], true, true); + $image = LiveLinks::getImage($liveLinks['id']); + } + + $rows[$key]['image'] = $image; + $rows[$key]['totalVideosOnChilds'] = $totalVideosOnChilds; + $rows[$key]['childs'] = $childs; + $rows[$key]['photo'] = $photo; + $rows[$key]['photoBg'] = $photoBg; + $rows[$key]['link'] = $link; + } + //array_multisort(array_column($rows, 'hierarchyAndName'), SORT_ASC, $rows); + $totalRows = Category::getTotalCategories(); + $obj->totalRows = $totalRows; + $obj->rows = $rows; + return new ApiObject("", false, $obj); + } + + /** + * Retrieves a specific video from a program (playlist) based on its index. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize the request. + * @type int|null $playlists_id Optional. The ID of the playlist (program) to retrieve the video from. + * @type int|null $index Optional. The index (position) of the video within the playlist. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=1&index=2&APISecret={APISecret} + * + * @return \ApiObject Object containing video details and channel metadata. + */ + /** + * @OA\Get( + * path="/api/video_from_program", + * summary="Get Video from program", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_video_from_program($parameters) + { + global $global; + $playlists = AVideoPlugin::loadPlugin("PlayLists"); + if (empty($parameters['playlists_id'])) { + //return new ApiObject("Playlist ID is empty", true, $parameters); + $_POST['sort']['created'] = 'DESC'; + $videos = PlayList::getVideosIDFromPlaylistLight(0); + } else { + $videos = PlayLists::getOnlyVideosAndAudioIDFromPlaylistLight($parameters['playlists_id']); + } + if (empty($videos)) { + return new ApiObject("There are no videos for this playlist", true, $parameters); + } + + if (empty($parameters['playlists_id'])) { + //return new ApiObject("Playlist ID is empty", true, $parameters); + $_POST['sort']['created'] = 'DESC'; + $config = new AVideoConf(); + $videos = Video::getAllVideos(); + $playlist = new PlayList($parameters['playlists_id']); + $parameters['playlist_name'] = __('Date Added'); + $parameters['modified'] = date('Y-m-d H:i:s'); + $parameters['users_id'] = 0; + $parameters['channel_name'] = $config->getWebSiteTitle(); + $parameters['channel_photo'] = $config->getFavicon(true); + $parameters['channel_bg'] = $config->getFavicon(true); + $parameters['channel_link'] = $global['webSiteRootURL']; + } else { + $playlist = new PlayList($parameters['playlists_id']); + $parameters['playlist_name'] = $playlist->getNameOrSerieTitle(); + $parameters['modified'] = $playlist->getModified(); + $users_id = $playlist->getUsers_id(); + $user = new User($users_id); + $parameters['users_id'] = $users_id; + $parameters['channel_name'] = $user->getChannelName(); + $parameters['channel_photo'] = $user->getPhotoDB(); + $parameters['channel_bg'] = $user->getBackground(); + $parameters['channel_link'] = $user->getChannelLink(); + } + + $parameters = array_merge($parameters, PlayLists::videosToPlaylist($videos, @$parameters['index'], !empty($parameters['audioOnly']))); + + return new ApiObject("", false, $parameters); + } + + /** + * Retrieves a specific audio track from a program (playlist) based on its index. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize the request. + * @type int|null $playlists_id Optional. The ID of the playlist (program) to retrieve the audio from. + * @type int|null $index Optional. The index (position) of the audio within the playlist. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=1&index=2&APISecret={APISecret} + * + * @return \ApiObject Object containing audio details and channel metadata. + */ + /** + * @OA\Get( + * path="/api/audio_from_program", + * summary="Get Audio from program", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_audio_from_program($parameters) + { + $parameters['audioOnly'] = 1; + return $this->get_api_video_from_program($parameters); + } + + /** + * Retrieves a list of suggested programs (playlists) including a default "Date Added" group. + * + * @param array $parameters Currently not used but reserved for future filtering or customization. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing a list of suggested playlists with channel and video information. + */ + /** + * @OA\Get( + * path="/api/suggested_programs", + * summary="Get Suggested programs", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_suggested_programs($parameters) + { + global $global; + $playlists = AVideoPlugin::loadPlugin("PlayLists"); + //var_dump($videos);exit; + $config = new AVideoConf(); + $users_id = User::getId(); + $list = []; + $obj = new stdClass(); + $obj->id = 0; + $obj->photo = $config->getFavicon(true); + $obj->channelLink = $global['webSiteRootURL']; + $obj->username = $config->getWebSiteTitle(); + $obj->name = __('Date added'); + $obj->link = $global['webSiteRootURL']; + $_POST['sort']['created'] = 'DESC'; + $obj->videos = PlayList::getVideosIDFromPlaylistLight(0); + $list[] = $obj; + $videos = PlayList::getSuggested(); + foreach ($videos as $value) { + $videosArrayId = PlayList::getVideosIdFromPlaylist($value['serie_playlists_id']); + if (empty($videosArrayId) || $value['status'] == "favorite" || $value['status'] == "watch_later") { + continue; + } + $obj = new stdClass(); + $obj->id = $value['serie_playlists_id']; + $obj->photo = User::getPhoto($value['users_id']); + $obj->channelLink = User::getChannelLink($value['users_id']); + $obj->username = User::getNameIdentificationById($value['users_id']); + $obj->name = $value['title']; + $obj->link = PlayLists::getLink($value['serie_playlists_id']); + $obj->videos = PlayList::getVideosIDFromPlaylistLight($value['serie_playlists_id']); + $list[] = $obj; + } + return new ApiObject("", false, $list); + } + + /** + * Returns all tags from the VideoTags plugin and lists the latest videos from the tags the user is subscribed to. + * + * @param array $parameters { + * @type int|bool|null $audioOnly Optional. If set to 1 or true, the response will include audio-only (MP3) versions of the videos. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing tag information and related videos, if subscribed. + */ + /** + * @OA\Get( + * path="/api/tags", + * summary="Get Tags", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_tags($parameters) + { + global $global; + $vtags = AVideoPlugin::loadPluginIfEnabled("VideoTags"); + + if (empty($vtags)) { + return new ApiObject("VideoTags is disabled"); + } + + $tags = VideoTags::getAll(User::getId()); + if (is_array($tags)) { + foreach ($tags as $key => $row) { + $tags[$key]['videos'] = array(); + $tags[$key]['photo'] = ImagesPlaceHolders::getVideoPlaceholder(ImagesPlaceHolders::$RETURN_URL); + if (!empty($row['subscription'])) { + $videos = TagsHasVideos::getAllVideosFromTagsId($row['id']); + $tags[$key]['videos'] = PlayLists::videosToPlaylist($videos, @$parameters['index'], !empty($parameters['audioOnly'])); + if (!empty($tags[$key]['videos'][0])) { + $tags[$key]['photo'] = $tags[$key]['videos'][0]['images']['poster']; + } + } + } + } else { + $tags = array(); + } + + return new ApiObject("", false, $tags); + } + + /** + * Returns detailed information and file sources for a specific video. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize access to video details or list all the videos. + * @type int $videos_id Required. The ID of the video to retrieve. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&APISecret={APISecret} + * + * @return \ApiObject Object containing video metadata, duration, file path, sources, and images. + */ + /** + * @OA\Get( + * path="/api/video_file", + * summary="Get Video file", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_video_file($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + $obj->videos_id = $parameters['videos_id']; + if (self::isAPISecretValid()) { + if (!User::canWatchVideoWithAds($obj->videos_id)) { + return new ApiObject("You cannot watch this video"); + } + } + $video = new Video('', '', $obj->videos_id); + $obj->filename = $video->getFilename(); + $obj->duration_in_seconds = $video->getDuration_in_seconds(); + $obj->title = $video->getTitle(); + $obj->video_file = Video::getHigherVideoPathFromID($obj->videos_id); + $obj->sources = getSources($obj->filename, true); + $obj->images = Video::getImageFromFilename($obj->filename); + return new ApiObject("", false, $obj); + } + + /** + * Checks if a specific user can watch a given video, with or without ads. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to check. + * @type int $users_id Required. The ID of the user to verify access for. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing access permission flags for the specified user and video. + */ + /** + * @OA\Get( + * path="/api/user_can_watch_video", + * summary="Get User can watch video", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_user_can_watch_video($parameters) + { + + $obj = new stdClass(); + $obj->users_id = intval($parameters['users_id']); + $obj->videos_id = intval($parameters['videos_id']); + $obj->userCanWatchVideo = false; + $obj->userCanWatchVideoWithAds = false; + $error = true; + $msg = ''; + + if (!empty($obj->videos_id)) { + $error = false; + $obj->userCanWatchVideo = AVideoPlugin::userCanWatchVideo($obj->users_id, $obj->videos_id); + $obj->userCanWatchVideoWithAds = AVideoPlugin::userCanWatchVideoWithAds($obj->users_id, $obj->videos_id); + } else { + $msg = 'Videos id is required'; + } + + + return new ApiObject($msg, $error, $obj); + } + + + /** + * Verifies whether the provided password is correct for a given video. + * If the video has no password, it will return true. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to check. + * @type string $video_password Required. The password provided by the user. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object indicating whether the password is correct for the video. + */ + /** + * @OA\Get( + * path="/api/video_password_is_correct", + * summary="Get Video password is correct", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_video_password_is_correct($parameters) + { + + $obj = new stdClass(); + $obj->videos_id = intval($parameters['videos_id']); + $obj->passwordIsCorrect = true; + $error = true; + $msg = ''; + + if (!empty($obj->videos_id)) { + $error = false; + $video = new Video('', '', $obj->videos_id); + $password = $video->getVideo_password(); + if (!empty($password)) { + $obj->passwordIsCorrect = $password == $parameters['video_password']; + } + } else { + $msg = 'Videos id is required'; + } + + + return new ApiObject($msg, $error, $obj); + } + + /** + * Retrieves all Pay-Per-View (PPV) plans associated with a specific video. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve PPV plans for. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2 + * + * @return \ApiObject Object containing the list of PPV plans and formatted pricing. + */ + /** + * @OA\Get( + * path="/api/ppv_plans", + * summary="Get Ppv plans", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_ppv_plans($parameters) + { + global $global; + $obj = new stdClass(); + $error = true; + $msg = ''; + + $objPPV = AVideoPlugin::getObjectDataIfEnabled('PayPerView'); + if (empty($objPPV)) { + return new ApiObject('PayPerView is disabled'); + } + + $objWallet = AVideoPlugin::getObjectDataIfEnabled('YPTWallet'); + if (empty($objWallet)) { + return new ApiObject('YPTWallet is disabled'); + } + + $obj->videos_id = intval($parameters['videos_id']); + if (empty($obj->videos_id)) { + return new ApiObject('videos_id is empty'); + } + + $obj->ppv = PayPerView::getAllPlansFromVideo($obj->videos_id); + //var_dump($obj->ppv); + foreach ($obj->ppv as $key => $value) { + $obj->ppv[$key]['valueString'] = YPTWallet::formatCurrency($value['value']); + } + if (!empty($obj->ppv)) { + $error = false; + } + return new ApiObject($msg, $error, $obj); + } + + /** + * Processes the purchase of a Pay-Per-View (PPV) plan for a video and deducts the value from the user's wallet. + * Verifies if the user is logged in and has sufficient balance, then returns the updated plan status and wallet info. + * + * @param array $parameters { + * @type int $plans_id Required. The ID of the PPV plan to be purchased. + * @type int $videos_id Required. The ID of the video to be unlocked. + * @type string $user Optional. Username of the user (used for authentication if session is not set). + * @type string $pass Optional. Password of the user (used for authentication if session is not set). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2&plans_id=4 + * + * @return \ApiObject Object with purchase result, user info, and plan details. + */ + /** + * @OA\Post( + * path="/api/ppv_buy", + * summary="Set Ppv buy", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_ppv_buy($parameters) + { + global $global; + $obj = new stdClass(); + $error = true; + $msg = ''; + $obj->users_id = User::getId(); + if (empty($obj->users_id)) { + return new ApiObject('You must login'); + } + + $objPPV = AVideoPlugin::getObjectDataIfEnabled('PayPerView'); + if (empty($objPPV)) { + return new ApiObject('PayPerView is disabled'); + } + + $objWallet = AVideoPlugin::getObjectDataIfEnabled('YPTWallet'); + if (empty($objWallet)) { + return new ApiObject('YPTWallet is disabled'); + } + + $obj->videos_id = intval($parameters['videos_id']); + if (empty($obj->videos_id)) { + return new ApiObject('videos_id is empty'); + } + + $obj->plans_id = intval($parameters['plans_id']); + if (empty($obj->plans_id)) { + return new ApiObject('plans_id is empty'); + } + + $obj->plan = PPV_Plans::getFromDb($obj->plans_id); + if (empty($obj->plan)) { + return new ApiObject('PPV plan does not exists'); + } + $error = false; + $obj->plans = PayPerView::getActivePlan($obj->users_id, $obj->videos_id); + if (empty($obj->plans)) { + $obj->plans = PayPerView::buyPPV(User::getId(), $obj->plans_id, $obj->videos_id); + $error = $obj->plans->error; + $msg = $obj->plans->msg; + } else { + $error = false; + } + return new ApiObject($msg, $error, $obj); + } + + /** + * Retrieves all subscription plans associated with a specific video. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve subscription plans for. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2 + * + * @return \ApiObject Object containing the list of available subscription plans for the video. + */ + /** + * @OA\Get( + * path="/api/subscription_plans", + * summary="Get Subscription plans", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_subscription_plans($parameters) + { + global $global; + $obj = new stdClass(); + $error = true; + $msg = ''; + + $objPPV = AVideoPlugin::getObjectDataIfEnabled('Subscription'); + if (empty($objPPV)) { + return new ApiObject('Subscription is disabled'); + } + + $objWallet = AVideoPlugin::getObjectDataIfEnabled('YPTWallet'); + if (empty($objWallet)) { + return new ApiObject('YPTWallet is disabled'); + } + + $obj->videos_id = intval($parameters['videos_id']); + if (empty($obj->videos_id)) { + return new ApiObject('videos_id is empty'); + } + + $sub = new Subscription(); + $obj->plans = $sub->getPlansFromVideo($obj->videos_id); + if (!empty($obj->plans)) { + $error = false; + } + return new ApiObject($msg, $error, $obj); + } + + /** + * Processes the purchase of a subscription plan for a video and deducts the value from the user's wallet. + * Verifies if the user is logged in and has sufficient balance, then returns the subscription status. + * + * @param array $parameters { + * @type int $plans_id Required. The ID of the subscription plan to be purchased. + * @type int $videos_id Required. The ID of the video to associate with the subscription. + * @type string $user Optional. Username of the user (used for authentication if session is not set). + * @type string $pass Optional. Password of the user (used for authentication if session is not set). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&plans_id=2 + * + * @return \ApiObject Object with subscription result, user info, and plan details. + */ + /** + * @OA\Post( + * path="/api/subscription_buy", + * summary="Set Subscription buy", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_subscription_buy($parameters) + { + global $global; + $obj = new stdClass(); + $error = true; + $msg = ''; + $obj->users_id = User::getId(); + if (empty($obj->users_id)) { + return new ApiObject('You must login'); + } + + $objPPV = AVideoPlugin::getObjectDataIfEnabled('Subscription'); + if (empty($objPPV)) { + return new ApiObject('Subscription is disabled'); + } + + $objWallet = AVideoPlugin::getObjectDataIfEnabled('YPTWallet'); + if (empty($objWallet)) { + return new ApiObject('YPTWallet is disabled'); + } + + $obj->videos_id = intval($parameters['videos_id']); + if (empty($obj->videos_id)) { + return new ApiObject('videos_id is empty'); + } + + $obj->plans_id = intval($parameters['plans_id']); + if (empty($obj->plans_id)) { + return new ApiObject('plans_id is empty'); + } + + $obj->plans = SubscriptionPlansTable::getFromDb($obj->plans_id); + if (empty($obj->plan)) { + return new ApiObject('Plan does not exists'); + } + + // check if the user has a valid plan for this video + $obj->activePlan = SubscriptionTable::getSubscription($obj->users_id, $obj->plans_id); + if (empty($obj->activePlan)) { + $obj->activePlan = Subscription::renew($obj->users_id, $obj->plans_id); + $error = empty($obj->activePlan); + } else { + $error = false; + } + return new ApiObject($msg, $error, $obj); + } + + /** + * Retrieves a list of videos with advanced filtering and sorting options. + * Supports filtering by category, tag, user/channel, type, and more. + * Includes pagination, playlist-compatible format, subtitles, comments, related videos, and advertising data. + * + * @param array $parameters { + * @type string $APISecret Optional. If provided and valid, returns all videos regardless of restrictions. + * @type array|string $sort Optional. Sorting options (e.g., sort[created]=desc, sort[trending]=1). + * @type int $videos_id Optional. The ID of a specific video to retrieve. + * @type string $clean_title Optional. The clean title of a specific video to retrieve. + * @type int $rowCount Optional. Maximum number of rows to return (for pagination). + * @type int $current Optional. Current page number (required for 'trending' sort to work properly). + * @type string $searchPhrase Optional. Search phrase to filter categories. + * @type int $tags_id Optional. ID of a tag to filter videos by. + * @type string|array $catName Optional. Clean name(s) of category(ies) to include. + * @type string|array $doNotShowCats Optional. Clean name(s) of category(ies) to exclude. + * @type string $channelName Optional. Filter videos by channel name. + * @type int|bool $playlist Optional. If set to 1, returns data in playlist-compatible format. + * @type string $videoType Optional. Type of video (e.g., 'video', 'audio', 'serie', 'embed', etc.). + * @type int|bool $is_serie Optional. 0 to return only videos, 1 for series, unset for all. + * @type int|bool $noRelated Optional. If set, disables fetching related videos. + * } + * + * @example {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&catName=default&rowCount=10 + * @example Suggested → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[suggested]=1 + * @example DateAdded → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[created]=desc + * @example Trending → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[trending]=1¤t=1 + * @example Shorts → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[shorts]=1 + * @example MostWatched→ {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[views_count]=desc + * + * @return \ApiObject Object containing video rows, metadata, and related details such as comments, subtitles, ads, and images. + */ + /** + * @OA\Get( + * path="/api/video", + * summary="Get Video", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_video($parameters) + { + $start = microtime(true); + /* + $cacheParameters = array('noRelated', 'APIName', 'catName', 'rowCount', 'APISecret', 'sort', 'searchPhrase', 'current', 'tags_id', 'channelName', 'videoType', 'is_serie', 'user', 'videos_id', 'playlist'); + + $cacheVars = array('users_id' => User::getId(), 'requestUniqueString'=>getRequestUniqueString()); + foreach ($cacheParameters as $value) { + $cacheVars[$value] = @$_REQUEST[$value]; + } + */ + + // use 1 hour cache + $videosListCache = new VideosListCacheHandler(); + //$cacheName = 'get_api_video' . md5(json_encode($cacheVars)); + if (empty($parameters['videos_id'])) { + //$obj = ObjectYPT::getCacheGlobal($cacheName, 3600); + $obj = $videosListCache->getCacheWithAutoSuffix(3600); + if (!empty($obj)) { + $end = microtime(true) - $start; + return new ApiObject("Cached response in {$end} seconds", false, $obj); + } + } + + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['videos_id'])) { + $status = Video::SORT_TYPE_VIEWABLE; + $ignoreGroup = false; + if (self::isAPISecretValid()) { + $status = ""; + $ignoreGroup = true; + } + // getVideo($id = "", $status = Video::SORT_TYPE_VIEWABLE, $ignoreGroup = false, $random = false, $suggestedOnly = false, $showUnlisted = false, $ignoreTags = false, $activeUsersOnly = true) + $rows = [Video::getVideo($parameters['videos_id'], $status, $ignoreGroup, false, false, true)]; + $totalRows = empty($rows) ? 0 : 1; + } elseif (self::isAPISecretValid()) { + $rows = Video::getAllVideos(Video::SORT_TYPE_VIEWABLE, false, true); + $totalRows = Video::getTotalVideos(Video::SORT_TYPE_VIEWABLE, false, true); + } elseif (!empty($parameters['clean_title'])) { + $rows = Video::getVideoFromCleanTitle($parameters['clean_title']); + $totalRows = empty($rows) ? 0 : 1; + } else { + $rows = Video::getAllVideos(); + $totalRows = Video::getTotalVideos(); + /* + if(!empty($_REQUEST['debug'])){ + global $_lastGetAllSQL; + global $lastGetTotalVideos; + var_dump($totalRows, $lastGetTotalVideos, $_lastGetAllSQL);exit; + } + */ + } + + if (!empty($_REQUEST['catName']) && empty($parameters['videos_id'])) { + $currentCat = Category::getCategoryByName($_REQUEST['catName']); + if (!empty($currentCat)) { + if (empty($parameters['videoType'])) { + $liveVideos = getLiveVideosFromCategory($currentCat['id']); + if (!empty($liveVideos)) { + $rows = array_merge($liveVideos, $rows); + $totalRows += count($liveVideos); + } + } + + $fullTotals = Category::getTotalFromCategory($currentCat['id'], false, true, true); + $totals = Category::getTotalFromCategory($currentCat['id']); + $currentCat['total'] = $totals['total']; + $currentCat['fullTotal'] = $fullTotals['total']; + $currentCat['fullTotal_videos'] = $fullTotals['videos']; + $currentCat['fullTotal_lives'] = $fullTotals['lives']; + $currentCat['fullTotal_livelinks'] = $fullTotals['livelinks']; + $currentCat['fullTotal_livelinks'] = $fullTotals['livelinks']; + + $currentCat['totalVideosOnChilds'] = Category::getTotalFromChildCategory($currentCat['id']); + $currentCat['childs'] = Category::getChildCategories($currentCat['id']); + $currentCat['photo'] = Category::getCategoryPhotoPath($currentCat['id']); + $currentCat['photoBg'] = Category::getCategoryBackgroundPath($currentCat['id']); + $currentCat['link'] = $global['webSiteRootURL'] . 'cat/' . $currentCat['clean_name']; + + foreach ($currentCat['childs'] as $key => $child) { + $endpoint = "{$global['webSiteRootURL']}plugin/API/get.json.php?APIName=video&catName={$child['clean_name']}"; + $currentCat['childs'][$key]['section'] = new SectionFirstPage('SubCategroy', $child['name'], $endpoint, getRowCount()); + } + $obj->category = $currentCat; + } + } + + unsetSearch(); + $objMob = AVideoPlugin::getObjectData("MobileManager"); + $SubtitleSwitcher = AVideoPlugin::getDataObjectIfEnabled("SubtitleSwitcher"); + + // check if there are custom ads for this video + $objAds = AVideoPlugin::getDataObjectIfEnabled('ADs'); + + foreach ($rows as $key => $value) { + if (is_object($value)) { + $value = object_to_array($value); + } + if (empty($value['filename'])) { + continue; + } + if ($value['type'] == Video::$videoTypeSerie) { + require_once $global['systemRootPath'] . 'objects/playlist.php'; + $rows[$key]['playlist'] = PlayList::getVideosFromPlaylist($value['serie_playlists_id']); + //var_dump($rows[$key]['playlist']);exit; + } + $images = Video::getImageFromFilename($rows[$key]['filename'], $rows[$key]['type']); + $rows[$key]['images'] = $images; + if ($rows[$key]['type'] !== Video::$videoTypeLinkVideo) { + $rows[$key]['videos'] = Video::getVideosPaths($value['filename'], true); + } else { + $extension = getExtension($rows[$key]['videoLink']); + $rows[$key]['videoLink'] = AVideoPlugin::modifyURL($rows[$key]['videoLink'], $rows[$key]['id']); + if ($extension == 'mp4') { + $rows[$key]['videos'] = array( + 'mp4' => array( + '720' => $rows[$key]['videoLink'] + ) + ); + } else if ($extension == 'm3u8') { + $rows[$key]['videos'] = array( + 'm3u8' => array( + 'url' => $rows[$key]['videoLink'], + 'url_noCDN' => $rows[$key]['videoLink'], + 'type' => 'video', + 'format' => 'm3u8', + 'resolution' => 'auto', + ) + ); + } + } + $rows[$key]['sources'] = array(); + if (empty($rows[$key]['videos'])) { + $rows[$key]['videos'] = new stdClass(); + } else { + $rows[$key]['sources'] = Video::getVideosPathsToSource($rows[$key]['videos']); + } + + $rows[$key]['Poster'] = !empty($objMob->portraitImage) ? $images->posterPortrait : $images->poster; + $rows[$key]['Thumbnail'] = !empty($objMob->portraitImage) ? $images->posterPortraitThumbs : $images->thumbsJpg; + $rows[$key]['imageClass'] = !empty($objMob->portraitImage) ? "portrait" : "landscape"; + $rows[$key]['createdHumanTiming'] = humanTiming(strtotime($rows[$key]['created'])); + $rows[$key]['pageUrl'] = Video::getLink($rows[$key]['id'], $rows[$key]['clean_title'], false); + $rows[$key]['embedUrl'] = Video::getLink($rows[$key]['id'], $rows[$key]['clean_title'], true); + $rows[$key]['UserPhoto'] = User::getPhoto($rows[$key]['users_id']); + $rows[$key]['isSubscribed'] = false; + + //make playlist compatible + if (!empty($parameters['playlist'])) { + $rows[$key]['mp3'] = convertVideoToMP3FileIfNotExists($value['id']); + $rows[$key]['category_name'] = $value['category']; + $rows[$key]['category'] = array('name' => $rows[$key]['category_name']); + $rows[$key]['channel_name'] = User::_getChannelName($rows[$key]['users_id']);; + } + + if (User::isLogged()) { + require_once $global['systemRootPath'] . 'objects/subscribe.php'; + $rows[$key]['isSubscribed'] = Subscribe::isSubscribed($rows[$key]['users_id']); + } + + + $sub = self::getSubtitle($value['filename']); + + $rows[$key]['subtitles_available'] = $sub['subtitles_available']; + $rows[$key]['subtitles'] = $sub['subtitles']; + $rows[$key]['subtitlesSRT'] = $sub['subtitlesSRT']; + + require_once $global['systemRootPath'] . 'objects/comment.php'; + require_once $global['systemRootPath'] . 'objects/subscribe.php'; + unset($_POST['sort']); + unset($_POST['current']); + unset($_POST['searchPhrase']); + $_REQUEST['rowCount'] = 10; + $_POST['sort']['created'] = "desc"; + $rows[$key]['comments'] = Comment::getAllComments($rows[$key]['id']); + $rows[$key]['commentsTotal'] = Comment::getTotalComments($rows[$key]['id']); + foreach ($rows[$key]['comments'] as $key2 => $value2) { + $user = new User($value2['users_id']); + $rows[$key]['comments'][$key2]['userPhotoURL'] = User::getPhoto($rows[$key]['comments'][$key2]['users_id']); + $rows[$key]['comments'][$key2]['userName'] = $user->getNameIdentificationBd(); + } + $rows[$key]['subscribers'] = Subscribe::getTotalSubscribes($rows[$key]['users_id']); + + //wwbn elements + $rows[$key]['wwbnURL'] = $rows[$key]['pageUrl']; + $rows[$key]['wwbnEmbedURL'] = $rows[$key]['embedUrl']; + $rows[$key]['wwbnImgThumbnail'] = $rows[$key]['Thumbnail']; + $rows[$key]['wwbnImgPoster'] = $rows[$key]['Poster']; + //$rows[$key]['wwbnImgGif'] = $rows[$key]['pageUrl']; + //$rows[$key]['wwbnTags'] = $rows[$key]['pageUrl']; + $rows[$key]['wwbnTitle'] = $rows[$key]['title']; + //$rows[$key]['wwbnDescription'] = $rows[$key]['description']; + //$rows[$key]['wwbnChannel'] = User::getChannelLink($rows[$key]['users_id']); + $rows[$key]['wwbnChannelURL'] = User::getChannelLink($rows[$key]['users_id']); + $rows[$key]['wwbnImgChannel'] = $rows[$key]['UserPhoto']; + //$rows[$key]['wwbnProgram'] = $rows[$key]['pageUrl']; + //$rows[$key]['wwbnProgramURL'] = $rows[$key]['pageUrl']; + $rows[$key]['wwbnType'] = $rows[$key]['type']; + + if (empty($parameters['noRelated'])) { + $rows[$key]['relatedVideos'] = Video::getRelatedMovies($rows[$key]['id']); + foreach ($rows[$key]['relatedVideos'] as $key2 => $value2) { + $rows[$key]['relatedVideos'][$key2]['tags'] = Video::getTags($value2['id']); + + $sub = self::getSubtitle($rows[$key]['relatedVideos'][$key2]['filename']); + + $rows[$key]['relatedVideos'][$key2]['subtitles_available'] = $sub['subtitles_available']; + $rows[$key]['relatedVideos'][$key2]['subtitles'] = $sub['subtitles']; + $rows[$key]['relatedVideos'][$key2]['subtitlesSRT'] = $sub['subtitlesSRT']; + + if (AVideoPlugin::isEnabledByName("VideoTags")) { + $rows[$key]['relatedVideos'][$key2]['videoTags'] = Tags::getAllFromVideosId($value2['id']); + $rows[$key]['relatedVideos'][$key2]['videoTagsObject'] = Tags::getObjectFromVideosId($value2['id']); + } + if ($rows[$key]['relatedVideos'][$key2]['type'] !== Video::$videoTypeLinkVideo) { + $rows[$key]['relatedVideos'][$key2]['videos'] = Video::getVideosPaths($value2['filename'], true); + } else if (preg_match('/m3u8/', $rows[$key]['relatedVideos'][$key2]['videoLink'])) { + $url = AVideoPlugin::modifyURL($rows[$key]['relatedVideos'][$key2]['videoLink']); + $rows[$key]['relatedVideos'][$key2]['videos']['m3u8']['url'] = $url; + $rows[$key]['relatedVideos'][$key2]['videos']['m3u8']['url_noCDN'] = $url; + $rows[$key]['relatedVideos'][$key2]['videos']['m3u8']['type'] = 'video'; + $rows[$key]['relatedVideos'][$key2]['videos']['m3u8']['format'] = 'm3u8'; + $rows[$key]['relatedVideos'][$key2]['videos']['m3u8']['resolution'] = 'auto'; + } + if (!empty($rows[$key]['relatedVideos'][$key2]['videos'])) { + $rows[$key]['relatedVideos'][$key2]['sources'] = Video::getVideosPathsToSource($rows[$key]['relatedVideos'][$key2]['videos']); + } + if (!empty($rows[$key]['relatedVideos'][$key2]['videoLink'])) { + $rows[$key]['relatedVideos'][$key2]['videoLink'] = AVideoPlugin::modifyURL($rows[$key]['relatedVideos'][$key2]['videoLink'], $value2['id']); + } + } + } + $rows[$key]['adsImages'] = array(); + if (!empty($objAds)) { + foreach (ADs::AdsPositions as $value) { + $type = $value[0]; + $rows[$key]['adsImages'][] = array('type' => $type, 'assets' => ADs::getAdsFromVideosId($type, $rows[$key]['id'])); + } + } + } + $obj->totalRows = $totalRows; + + if (!empty($parameters['playlist'])) { + $obj->videos = $rows; + } else { + $obj->rows = $rows; + } + $obj = self::addRowInfo($obj); + //var_dump($obj->rows );exit; + //ObjectYPT::setCacheGlobal($cacheName, $obj); + $videosListCache->setCache($obj); + return new ApiObject("", false, $obj); + } + + private static function getSubtitle($filename) + { + global $_SubtitleSwitcher; + if (!isset($_SubtitleSwitcher)) { + $_SubtitleSwitcher = AVideoPlugin::getDataObjectIfEnabled("SubtitleSwitcher"); + } + $row = array(); + $row['subtitles_available'] = false; + $row['subtitles'] = []; + $row['subtitlesSRT'] = []; + if ($_SubtitleSwitcher) { + $subtitles = getVTTTracks($filename, true); + $row['subtitles_available'] = !empty($subtitles); + if (empty($_SubtitleSwitcher->disableOnAPI)) { + $row['subtitles'] = $subtitles; + foreach ($row['subtitles'] as $key2 => $value) { + $row['subtitlesSRT'][] = convertSRTTrack($value); + } + } + } + return $row; + } + + /** + * Updates video metadata such as title, description, category, privacy options, and more. + * Requires proper authentication (APISecret or valid user credentials with permission to edit the video). + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to update. + * @type string|null $user Optional. Username for authentication. + * @type string|null $pass Optional. Password for authentication. + * @type string|null $APISecret Optional. Valid API secret key for authentication. + * @type int|null $next_videos_id Optional. ID of the next suggested video. + * @type string|null $title Optional. New video title. + * @type string|null $status Optional. Video status (e.g., 'public', 'private'). + * @type string|null $description Optional. Video description. + * @type int|null $categories_id Optional. ID of the new category. + * @type int|null $can_download Optional. 1 to allow download, 0 to disallow. + * @type int|null $can_share Optional. 1 to allow sharing, 0 to disallow. + * @type int|null $only_for_paid Optional. 1 for paid-only access, 0 otherwise. + * @type string|null $video_password Optional. Password required to watch the video. + * @type string|null $trailer1 Optional. URL for the trailer. + * @type string|null $rrating Optional. Rating ('g', 'pg', 'pg-13', 'r', 'nc-17', 'ma'). + * @type string|null $created Optional. Custom creation date (requires admin or valid APISecret). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b + * + * @return \ApiObject Object containing the result of the update operation or an error message. + */ + /** + * @OA\Post( + * path="/api/video_save", + * summary="Set Video save", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_video_save($parameters) + { + global $advancedCustomUser; + + // Check if parameters array is not empty + if (empty($parameters)) { + return new ApiObject('Parameters array is empty'); + } + + // Check for the existence of the required key + if (empty($parameters['videos_id'])) { + return new ApiObject('videos_id is empty'); + } + + if (!Video::canEdit($parameters['videos_id']) && !Permissions::canModerateVideos() && !self::isAPISecretValid()) { + return new ApiObject('Permission denied'); + } + + $obj = new Video('', '', $parameters['videos_id'], true); + + if (empty($obj->getCreated())) { + return new ApiObject('Video not found'); + } + + if (isset($parameters['next_videos_id'])) { + $obj->setNext_videos_id($parameters['next_videos_id']); + } + + if (isset($parameters['description'])) { + $obj->setDescription($parameters['description']); + } + + if (!empty($advancedCustomUser->userCanNotChangeCategory) || Permissions::canModerateVideos()) { + if (isset($parameters['categories_id'])) { + $obj->setCategories_id($parameters['categories_id']); + } + } + + if (isset($parameters['can_download'])) { + $obj->setCan_download($parameters['can_download']); + } + + if (isset($parameters['can_share'])) { + $obj->setCan_share($parameters['can_share']); + } + + if (isset($parameters['only_for_paid'])) { + $obj->setOnly_for_paid($parameters['only_for_paid']); + } + + if (isset($parameters['video_password'])) { + $obj->setVideo_password($parameters['video_password']); + } + + if (isset($parameters['trailer1'])) { + $obj->setTrailer1($parameters['trailer1']); + } + + if (isset($parameters['rrating'])) { + $obj->setRrating($parameters['rrating']); + } + + if (Permissions::canAdminVideos() || self::isAPISecretValid()) { + if (isset($_REQUEST['created'])) { + $obj->setCreated($parameters['created']); + } + } + + if (!empty($parameters['title'])) { + $obj->setTitle($parameters['title']); + } + $id = $obj->save(false, true); + // set status must be after save videos parameters + if (!empty($parameters['status'])) { + $obj->setStatus($parameters['status']); + } + return new ApiObject("", false, $id); + } + + /** + * Retrieves the total count of videos, with optional filters based on categories, tags, and channel. + * If APISecret is valid, it lists all videos regardless of restrictions. + * + * @param array $parameters { + * @type string $APISecret Optional. If provided and valid, returns all videos. + * @type string $searchPhrase Optional. Phrase to search within the categories. + * @type int $tags_id Optional. ID of the tag to filter the videos. + * @type string $catName Optional. Clean API name of the category to filter videos by. + * @type string $channelName Optional. Name of the channel to filter videos by. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} + * + * @return \ApiObject Object containing the total count of videos. + */ + /** + * @OA\Get( + * path="/api/videosCount", + * summary="Get Videoscount", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_videosCount($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (self::isAPISecretValid()) { + $totalRows = Video::getTotalVideos(Video::SORT_TYPE_VIEWABLE, false, true); + } else { + $totalRows = Video::getTotalVideos(); + } + //$objMob = AVideoPlugin::getObjectData("MobileManager"); + //$SubtitleSwitcher = AVideoPlugin::loadPluginIfEnabled("SubtitleSwitcher"); + $obj->totalRows = $totalRows; + return new ApiObject("", false, $obj); + } + + /** + * Deletes a video by its ID if the authenticated user has permission or a valid APISecret is provided. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to delete. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username for authentication (if APISecret is not used). + * @type string $pass Optional. Password for authentication (if APISecret is not used). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} + * + * @return \ApiObject Object indicating success or failure of the delete operation. + */ + /** + * @OA\Get( + * path="/api/video_delete", + * summary="Get Video delete", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_video_delete($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['videos_id'])) { + if (!User::canUpload()) { + return new ApiObject("Access denied"); + } + if (!empty($_REQUEST['APISecret']) && !self::isAPISecretValid()) { + return new ApiObject("Secret does not match"); + } + $vid = new Video('', '', $parameters['videos_id'], true); + if (!$vid->userCanManageVideo()) { + return new ApiObject("User cannot manage the video"); + } + $id = $vid->delete(); + return new ApiObject("", !$id, $id); + } else { + return new ApiObject("Video ID is required"); + } + } + + /** + * Creates or updates a comment on a specific video. Requires user authentication or a valid APISecret. + * + * @param array $parameters { + * @type string $comment Required. The content of the comment. + * @type int $videos_id Required. The ID of the video that will receive the comment. + * @type int $id Optional. The ID of the comment to edit. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username of the user posting the comment. + * @type string $pass Optional. Password of the user posting the comment. + * @type int $comments_id Optional. The parent comment ID (for replies). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} + * + * @return \ApiObject Object containing the result and the new or updated comment ID. + */ + /** + * @OA\Post( + * path="/api/comment", + * summary="Set Comment", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_comment($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['videos_id'])) { + if (!empty($_REQUEST['APISecret']) && !self::isAPISecretValid()) { + return new ApiObject("Secret does not match"); + } elseif (!User::canComment()) { + return new ApiObject("Access denied"); + } + $parameters['comments_id'] = intval(@$parameters['comments_id']); + require_once $global['systemRootPath'] . 'objects/comment.php'; + if (!empty($parameters['id'])) { + $parameters['id'] = intval($parameters['id']); + if (Comment::userCanEditComment($parameters['id'])) { + $obj = new Comment("", 0, $parameters['id']); + $obj->setComment($parameters['comment']); + } + } else { + $obj = new Comment($parameters['comment'], $parameters['videos_id']); + $obj->setComments_id_pai($parameters['comments_id']); + } + $objResponse = new stdClass(); + $objResponse->comments_id = $obj->save(); + $objResponse->videos_id = $parameters['videos_id']; + return new ApiObject("", !$objResponse->comments_id, $objResponse); + } else { + return new ApiObject("Video ID is required"); + } + } + + /** + * Retrieves all comments for a given video, with optional authentication and pagination. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve comments for. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username for authentication. + * @type string $pass Optional. Password for authentication. + * @type int $rowCount Optional. Maximum number of comments to return. + * @type int $current Optional. Current page number for pagination. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} + * + * @return \ApiObject Object containing the list of comments with user metadata. + */ + /** + * @OA\Get( + * path="/api/comment", + * summary="Get Comment", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_comment($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['videos_id'])) { + if (!empty($_REQUEST['APISecret']) && !self::isAPISecretValid()) { + return new ApiObject("Secret does not match"); + } elseif (!User::canComment()) { + return new ApiObject("Access denied"); + } + + if (!User::canWatchVideo($parameters['videos_id'])) { + return new ApiObject("Cannot watch video"); + } + + require_once $global['systemRootPath'] . 'objects/comment.php'; + + $_POST['sort']['created'] = "desc"; + $obj = Comment::getAllComments($parameters['videos_id']); + $obj = Comment::addExtraInfo($obj); + return new ApiObject("", false, $obj); + } else { + return new ApiObject("Video ID is required"); + } + } + + /** + * Retrieves live schedule information. If a specific ID is provided, returns only that record. + * Requires user authentication. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. The ID of the live schedule to retrieve. + * @type string $user Required. Username for authentication. + * @type string $pass Required. Password for authentication. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing the live schedule record(s). + */ + /** + * @OA\Get( + * path="/api/live_schedule", + * summary="Get Live schedule", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_live_schedule($parameters) + { + if (!User::canStream()) { + return new ApiObject("You cannot stream"); + } else { + $users_id = User::getId(); + $_POST['sort'] = array('scheduled_time' => 'DESC'); + if (empty($parameters['live_schedule_id'])) { + $obj = Live_schedule::getAll($users_id); + } else { + $row = Live_schedule::getFromDb($parameters['live_schedule_id']); + if ($row['users_id'] != $users_id) { + return new ApiObject("This live schedule does not belong to you"); + } else { + $obj = $row; + } + } + } + return new ApiObject("", false, $obj); + } + + /** + * Retrieves one or more live schedule records. If `live_schedule_id` is provided, returns only the specified record. + * Requires user authentication. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. The ID of the specific live schedule to retrieve. + * @type string $user Required. Username of the user for authentication. + * @type string $pass Required. Password of the user for authentication. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing one or more live schedule entries. + */ + /** + * @OA\Post( + * path="/api/live_schedule_delete", + * summary="Set Live schedule delete", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_live_schedule_delete($parameters) + { + if (!User::canStream()) { + return new ApiObject("You cannot stream"); + } else { + $users_id = User::getId(); + if (empty($parameters['live_schedule_id'])) { + return new ApiObject("live_schedule_id cannot be empty"); + } else { + $row = new Live_schedule($parameters['live_schedule_id']); + if ($row->getUsers_id() != $users_id) { + return new ApiObject("This live schedule does not belong to you"); + } else { + $obj = $row->delete(); + } + } + } + return new ApiObject("", false, $obj); + } + + /** + * Creates or updates a live schedule record. Allows uploading poster images and setting metadata. + * Requires user authentication with streaming permissions. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. If provided, updates the existing record. + * @type int|null $live_servers_id Optional. Live server ID (default is 0). + * @type string|null $base64PNGImageRegular Optional. Base64-encoded regular poster image. + * @type string|null $base64PNGImagePreRoll Optional. Base64-encoded pre-roll poster image. + * @type string|null $base64PNGImagePostRoll Optional. Base64-encoded post-roll poster image. + * @type string $title Required when creating. Title of the live schedule. + * @type string|null $description Optional. Description of the live stream. + * @type string $scheduled_time Required when creating. Date and time in 'YYYY-mm-dd HH:ii:ss' format. + * @type string $status Required. 'a' for active or 'i' for inactive. + * @type string|null $scheduled_password Optional. Password to restrict access to the live stream. + * @type string $user Required. Username of the user. + * @type string $pass Required. Password of the user. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject Object containing the created or updated live_schedule_id. + */ + /** + * @OA\Post( + * path="/api/live_schedule", + * summary="Set Live schedule", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_live_schedule($parameters) + { + $live_schedule_id = 0; + $obj = new stdClass(); + if (!User::canStream()) { + return new ApiObject("You cannot stream"); + } else { + $users_id = User::getId(); + if (empty($parameters['live_schedule_id'])) { + if (empty($parameters['title'])) { + return new ApiObject("Title cannot be empty"); + } + if (empty($parameters['scheduled_time'])) { + return new ApiObject("scheduled_time cannot be empty"); + } + if (empty($parameters['status']) || $parameters['status'] !== 'i') { + $parameters['status'] = 'a'; + } + $o = new Live_schedule(0); + } else { + $o = new Live_schedule($parameters['live_schedule_id']); + if ($o->getUsers_id() != $users_id) { + return new ApiObject("This live schedule does not belong to you"); + } else { + $o = new Live_schedule($parameters['live_schedule_id']); + } + } + //var_dump($parameters);exit; + if (isset($parameters['title'])) { + $o->setTitle($parameters['title']); + } + if (isset($parameters['description'])) { + $o->setDescription($parameters['description']); + } + if (isset($parameters['live_servers_id'])) { + $o->setLive_servers_id($parameters['live_servers_id']); + } + if (isset($parameters['scheduled_time'])) { + $o->setScheduled_time($parameters['scheduled_time']); + } + if (isset($parameters['status'])) { + $o->setStatus($parameters['status']); + } + if (isset($parameters['scheduled_password'])) { + $o->setScheduled_password($parameters['scheduled_password']); + } + + $o->setUsers_id($users_id); + $live_schedule_id = $o->save(); + if ($live_schedule_id) { + if (!empty($parameters['base64PNGImageRegular'])) { + $image = Live_schedule::getPosterPaths($live_schedule_id, 0, Live::$posterType_regular); + saveBase64DataToPNGImage($parameters['base64PNGImageRegular'], $image['path']); + } + if (!empty($parameters['base64PNGImagePreRoll'])) { + $image = Live_schedule::getPosterPaths($live_schedule_id, 0, Live::$posterType_preroll); + saveBase64DataToPNGImage($parameters['base64PNGImagePreRoll'], $image['path']); + } + if (!empty($parameters['base64PNGImagePostRoll'])) { + $image = Live_schedule::getPosterPaths($live_schedule_id, 0, Live::$posterType_postroll); + saveBase64DataToPNGImage($parameters['base64PNGImagePostRoll'], $image['path']); + } + + + $o = new Live_schedule($live_schedule_id); + $obj->live_schedule_id = $live_schedule_id; + } + } + return new ApiObject("", empty($live_schedule_id), $obj); + } + + /** + * Retrieves live stream statistics using the Live plugin's `stats.json.php` endpoint. + * This will immediately return the JSON response and terminate execution. + * + * @param array $parameters Not used in this endpoint. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject This method exits early and does not return an ApiObject. The response is handled by `stats.json.php`. + */ + /** + * @OA\Get( + * path="/api/livestreams", + * summary="Get Livestreams", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_livestreams($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'plugin/Live/stats.json.php'; + exit; + } + + /** + * Creates or updates a livestream record for a user. Can optionally reset the livestream key. + * Requires authentication using either APISecret or user credentials. + * + * @param array $parameters { + * @type string|null $title Optional. The title of the livestream. + * @type int|null $public Optional. 1 for public listing, 0 for private/unlisted. + * @type string|null $APISecret Optional. If provided and valid, bypasses user authentication. + * @type int|null $users_id Optional. The user ID. Required if APISecret is used. + * @type int|null $resetKey Optional. Set to 1 to reset the livestream key. + * @type string|null $user Optional. Username for authentication (if APISecret is not used). + * @type string|null $pass Optional. Password for authentication (if APISecret is not used). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&users_id=1 + * + * @return \ApiObject Object containing the updated livestream data or an error message. + */ + /** + * @OA\Post( + * path="/api/livestream_save", + * summary="Set Livestream save", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_livestream_save($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (empty($parameters['title']) && !isset($parameters['public'])) { + return new ApiObject("Invalid parameters"); + } + if (self::isAPISecretValid() || User::isLogged()) { + if (!empty($parameters['users_id'])) { + if (!self::isAPISecretValid()) { + $parameters['users_id'] = User::getId(); + } + } else { + $parameters['users_id'] = User::getId(); + } + + $user = new User($parameters['users_id']); + if (empty($user->getUser())) { + return new ApiObject("User Not defined"); + } + $p = AVideoPlugin::loadPlugin("Live"); + + $trasnmition = LiveTransmition::createTransmitionIfNeed($parameters['users_id']); + $trans = new LiveTransmition($trasnmition['id']); + $trans->setTitle($parameters['title']); + $trans->setPublic($parameters['public']); + if ($obj->id = $trans->save()) { + if ($parameters['resetKey']) { + LiveTransmition::resetTransmitionKey($parameters['users_id']); + } + $trans = LiveTransmition::getFromDb($obj->id, true); + return new ApiObject("", false, $trans); + } else { + return new ApiObject("Error on save"); + } + } else { + return new ApiObject("API Secret is not valid"); + } + } + + /** + * Returns livestream and wallet information for a specific user. + * Requires authentication using either APISecret or user credentials. + * + * @param array $parameters { + * @type string|null $APISecret Optional. If provided and valid, bypasses user authentication. + * @type int|null $users_id Optional. The ID of the user. If not set, uses the logged-in user. + * @type string|null $user Optional. Username for authentication (if APISecret is not used). + * @type string|null $pass Optional. Password for authentication (if APISecret is not used). + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&users_id=1 + * + * @return \ApiObject Object containing user info, livestream settings, active/live/scheduled streams, and wallet info. + */ + /** + * @OA\Get( + * path="/api/user", + * summary="Get User", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_user($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (!empty($parameters['users_id'])) { + if (!self::isAPISecretValid()) { + $parameters['users_id'] = User::getId(); + } + } else { + $parameters['users_id'] = User::getId(); + } + + $user = new User($parameters['users_id']); + if (empty($user->getUser())) { + return new ApiObject("User Not found"); + } + $p = AVideoPlugin::loadPlugin("Live"); + + $obj->user = User::getUserFromID($user->getBdId()); + + unset($obj->user['externalOptions']); + unset($obj->user['extra_info']); + $obj->user['canStream'] = $obj->user['canStream'] || $obj->user['isAdmin']; + $obj->user['DonationButtons'] = _json_decode(@$obj->user['DonationButtons']); + + + $obj->livestream = LiveTransmition::createTransmitionIfNeed($user->getBdId()); + + $str = "{$obj->livestream['key']}"; + $encrypt = encryptString($str); + + $url = Live::getServer(); + + $obj->livestream["users_id"] = $user->getBdId(); + $obj->livestream["live_servers_id"] = Live::getCurrentLiveServersId(); + $obj->livestream["server"] = $p->getServer($obj->livestream["live_servers_id"]) . "?p=" . $user->getPassword(); + $obj->livestream["server_v2"] = Live::getRTMPLinkWithOutKey($user->getBdId()); + // those are for the ypt mobile app + $obj->livestream["server_v3"] = addLastSlash($url); + $obj->livestream["key_v3"] = "{$obj->livestream['key_with_index']}?s={$encrypt}"; + + $obj->livestream["poster"] = $global['webSiteRootURL'] . Live::getRegularPosterImage($user->getBdId(), $obj->livestream["live_servers_id"], 0, 0); + $obj->livestream["joinURL"] = Live::getLinkToLiveFromUsers_idAndLiveServer($user->getBdId(), $obj->livestream["live_servers_id"]); + + $obj->livestream["activeLives"] = array(); + $obj->livestream["latestLives"] = array(); + $obj->livestream["scheduledLives"] = array(); + $obj->wallet = array('isEnabled' => false, 'balance' => 0, 'balance_formated' => ''); + + if (AVideoPlugin::isEnabledByName('Live')) { + $rows = LiveTransmitionHistory::getActiveLiveFromUser($parameters['users_id'], '', '', 100); + + foreach ($rows as $value) { + $value['live_transmitions_history_id'] = $value['id']; + $value['joinURL'] = LiveTransmitionHistory::getLinkToLive($value['id']); + $value['isPrivate'] = LiveTransmitionHistory::isPrivate($value['id']); + $value['isPasswordProtected'] = LiveTransmitionHistory::isPasswordProtected($value['id']); + $value['isRebroadcast'] = LiveTransmitionHistory::isRebroadcast($value['id']); + $obj->livestream["activeLives"][] = $value; + } + + $rows = LiveTransmitionHistory::getLastsLiveHistoriesFromUser($parameters['users_id'], 5, true); + + foreach ($rows as $value) { + $value['live_transmitions_history_id'] = $value['id']; + $value['joinURL'] = LiveTransmitionHistory::getLinkToLive($value['id']); + $obj->livestream["latestLives"][] = $value; + } + + $rows = Live_schedule::getAllActiveLimit($parameters['users_id']); + + foreach ($rows as $value) { + $obj->livestream["scheduledLives"][] = $value; + } + } + + if ($walletObj = AVideoPlugin::loadPluginIfEnabled('YPTWallet')) { + $wallet = $walletObj->getOrCreateWallet($parameters['users_id']); + + $obj->wallet['isEnabled'] = true; + $obj->wallet['balance'] = $walletObj->getBalance($parameters['users_id']); + $obj->wallet['balance_formated'] = YPTWallet::formatCurrency($obj->wallet['balance'], false); + } + + return new ApiObject("", false, $obj); + } + + /** + * Returns a filtered list of users. Requires a valid APISecret. + * + * @param array $parameters { + * @type string $APISecret Required. API secret key for authentication. + * @type int|null $rowCount Optional. Maximum number of users to return. + * @type int|null $current Optional. Current page number for pagination. + * @type string|null $searchPhrase Optional. Search term to filter by username or name. + * @type string|null $status Optional. Filter by user status: 'a' (active) or 'i' (inactive). + * @type int|null $isAdmin Optional. Filter to return only admin users. + * @type int|null $isCompany Optional. Filter to return only company users. + * @type int|null $canUpload Optional. Filter to return only users who can upload. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&status=a&rowCount=3&searchPhrase=test + * + * @return \ApiObject Object containing the list of users matching the filters. + */ + /** + * @OA\Get( + * path="/api/users_list", + * summary="Get Users list", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_users_list($parameters) + { + global $global; + $obj = $this->startResponseObject($parameters); + if (self::isAPISecretValid()) { + $status = ''; + if (!empty($_GET['status'])) { + if ($_GET['status'] === 'i') { + $status = 'i'; + } else { + $status = 'a'; + } + } + $isAdmin = null; + if (!empty($_GET['isAdmin'])) { + $isAdmin = 1; + } + $isCompany = null; + if (!empty($_GET['isCompany'])) { + $isCompany = 1; + } + $canUpload = null; + if (!empty($_GET['canUpload'])) { + $canUpload = 1; + } + + + //getAllUsers($ignoreAdmin = false, $searchFields = ['name', 'email', 'user', 'channelName', 'about'], $status = "", $isAdmin = null, $isCompany = null, $canUpload = null) + $rows = User::getAllUsers(true, ['user', 'name'], $status, $isAdmin, $isCompany, $canUpload); + + return new ApiObject("", false, $rows); + } else { + return new ApiObject("API Secret is not valid"); + } + } + + /** + * Returns the total number of videos and their combined views count. + * + * @param array $parameters { + * @type string|null $APISecret Optional. If provided and valid, grants access to all videos. + * @type string|null $sort Optional. Sort by column name (e.g., views_count). + * @type int|null $videos_id Optional. Specific video ID to retrieve. + * @type string|null $clean_title Optional. Clean title to retrieve a specific video. + * @type int|null $rowCount Optional. Maximum number of rows to return. + * @type int|null $current Optional. Current page number for pagination. + * @type string|null $searchPhrase Optional. Search term for filtering. + * @type int|null $tags_id Optional. Filter by tag ID. + * @type string|null $catName Optional. Filter by category clean name. + * @type string|null $channelName Optional. Filter by channel name. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} + * + * @return \ApiObject Object containing totalRows and viewsCount for the filtered video set. + */ + /** + * @OA\Get( + * path="/api/videosViewsCount", + * summary="Get Videosviewscount", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_videosViewsCount($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + $obj = $this->startResponseObject($parameters); + if (self::isAPISecretValid()) { + $rows = Video::getAllVideos(Video::SORT_TYPE_VIEWABLE, false, true); + $totalRows = Video::getTotalVideos(Video::SORT_TYPE_VIEWABLE, false, true); + } elseif (!empty($parameters['videos_id'])) { + $rows = [Video::getVideo($parameters['videos_id'])]; + $totalRows = empty($rows) ? 0 : 1; + } elseif (!empty($parameters['clean_title'])) { + $rows = Video::getVideoFromCleanTitle($parameters['clean_title']); + $totalRows = empty($rows) ? 0 : 1; + } else { + $rows = Video::getAllVideos(); + $totalRows = Video::getTotalVideos(); + } + $objMob = AVideoPlugin::getObjectData("MobileManager"); + $viewsCount = 0; + foreach ($rows as $key => $value) { + if (is_object($value)) { + $value = object_to_array($value); + } + $viewsCount += $value['views_count']; + } + $obj->totalRows = $totalRows; + $obj->viewsCount = $viewsCount; + return new ApiObject("", false, $obj); + } + + /** + * Returns a list of all channels on the site. + * + * @param array $parameters Currently unused. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject List of channels with basic information: + * - id: Channel ID + * - photo: Channel photo URL + * - channelLink: Channel link URL + * - name: User Display name + * - channelName: Unique channel identifier + */ + /** + * @OA\Get( + * path="/api/channels", + * summary="Get Channels", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_channels($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/Channel.php'; + $channels = Channel::getChannels(); + $list = []; + foreach ($channels as $value) { + $obj = new stdClass(); + $obj->id = $value['id']; + $obj->photo = User::getPhoto($value['id']); + $obj->channelLink = User::getChannelLink($value['id']); + $obj->name = User::getNameIdentificationById($value['id']); + $obj->channelName = $value['channelName']; + + $list[] = $obj; + } + return new ApiObject("", false, $list); + } + + /** + * Returns a single Program (Playlist) from the site. + * + * @param array $parameters + * - playlists_id (int) Optional. The playlist ID. Required if videos_id is not provided. + * - videos_id (int) Optional. If provided, retrieves the program associated with the video (if it's part of a series). + * + * Behavior: + * - If `playlists_id` is provided, the program is returned only if it belongs to the logged-in user. + * - If `videos_id` is provided, the function will resolve the associated playlist and ensure the user has permission to watch it. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=12 + * + * @return \ApiObject Object with the list of videos in the program (property: videos). + */ + /** + * @OA\Get( + * path="/api/program", + * summary="Get Program", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_program($parameters) + { + global $global; + if (!empty($parameters['videos_id'])) { + $v = new Video('', '', $parameters['videos_id']); + $parameters['playlists_id'] = $v->getSerie_playlists_id(); + } + if (empty($parameters['playlists_id'])) { + return new ApiObject("playlists_id is required"); + } + require_once $global['systemRootPath'] . 'objects/playlist.php'; + $obj = new PlayList($parameters['playlists_id']); + if (empty($obj)) { + forbiddenPage(); + } + if (empty($parameters['videos_id'])) { + if (!empty($obj->getUsers_id())) { + forbidIfItIsNotMyUsersId($obj->getUsers_id()); + } + } else { + $cansee = User::canWatchVideoWithAds($parameters['videos_id']); + if (!$cansee) { + return new ApiObject("You cannot watch this video"); + } + } + $obj = new stdClass(); + $obj->videos = PlayList::getAllFromPlaylistsID($parameters['playlists_id']); + + return new ApiObject("", false, $obj); + } + + /** + * Returns all Programs (Playlists) available for the authenticated user. + * + * @param array $parameters + * - onlyWithVideos (int) Optional. 0 or 1. If 1, only returns programs that contain videos. Default is 1. + * - returnFavoriteAndWatchLater (int) Optional. 0 or 1. If 1, includes "favorite" and "watch later" programs. Default is 0. + * + * Notes: + * - The user must be logged in. + * - Each program includes metadata such as ID, photo, username, link, and associated videos. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject List of programs with metadata. + */ + /** + * @OA\Get( + * path="/api/programs", + * summary="Get Programs", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_programs($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/playlist.php'; + + $config = new AVideoConf(); + $users_id = User::getId(); + $list = []; + if (!empty($users_id)) { + //getAllFromUserLight($userId, $publicOnly = true, $status = false, $playlists_id = 0, $onlyWithVideos = false, $includeSeries = false) + $playlists = PlayList::getAllFromUserLight($users_id, false, false, 0, _empty($parameters['onlyWithVideos']) ? 0 : 1, true); + foreach ($playlists as $value) { + $videosArrayId = PlayList::getVideosIdFromPlaylist($value['id']); + if (!_empty($parameters['onlyWithVideos']) && empty($videosArrayId)) { + continue; + } + if (_empty($parameters['returnFavoriteAndWatchLater'])) { + if ($value['status'] == "favorite" || $value['status'] == "watch_later") { + continue; + } + } + $obj = new stdClass(); + $obj->id = $value['id']; + $obj->photo = User::getPhoto($value['users_id']); + $obj->channelLink = User::getChannelLink($value['users_id']); + $obj->username = User::getNameIdentificationById($value['users_id']); + $obj->name = $value['name']; + $obj->status = $value['status']; + $obj->link = PlayLists::getLink($value['id']); + $obj->videos = $value['videos']; + $list[] = $obj; + } + } + return new ApiObject("", false, $list); + } + + /** + * Create a new Program (Playlist). + * + * @param array $parameters + * - name (string, required) The name of the new program. + * - status (string, optional) The program visibility status. Accepted values: 'public', 'private', 'unlisted', 'favorite', 'watch_later'. + * + * Notes: + * - The user must be logged in. + * - The PlayLists plugin must be enabled. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&name=NewPL&status=unlisted + * + * @return \ApiObject Success or error status. + */ + /** + * @OA\Post( + * path="/api/create_programs", + * summary="Set Create programs", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_create_programs($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/playlist.php'; + $users_id = User::getId(); + if (empty($users_id)) { + return new ApiObject("You must login first"); + } + $array = array('name'); + foreach ($array as $value) { + if (empty($parameters[$value])) { + return new ApiObject("{$value} cannot be empty"); + } + } + + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin not enabled"); + } + + $playList = new PlayList(0); + $playList->setName($parameters['name']); + $playList->setStatus(@$parameters['status']); + + $obj = new stdClass(); + $obj->error = empty($playList->save()); + + return new ApiObject("", false, $obj); + } + + /** + * Delete an existing Program (Playlist). + * + * @param array $parameters + * - playlists_id (int, required) The ID of the program to delete. + * + * Notes: + * - The user must be logged in and must own the playlist. + * - The PlayLists plugin must be enabled. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=12 + * + * @return \ApiObject Success or error status. + */ + /** + * @OA\Post( + * path="/api/delete_programs", + * summary="Set Delete programs", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_delete_programs($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/playlist.php'; + $users_id = User::getId(); + if (empty($users_id)) { + return new ApiObject("You must login first"); + } + $array = array('playlists_id'); + foreach ($array as $value) { + if (empty($parameters[$value])) { + return new ApiObject("{$value} cannot be empty"); + } + } + + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin not enabled"); + } + + $playList = new PlayList($parameters['playlists_id']); + if (empty($playList) || User::getId() !== $playList->getUsers_id()) { + return new ApiObject("Permission denied"); + } + + $obj = new stdClass(); + $obj->error = empty($playList->delete()); + + return new ApiObject("", false, $obj); + } + + /** + * Add or remove a video from a Program (Playlist). + * + * @param array $parameters + * - videos_id (int, required) The ID of the video. + * - playlists_id (int, required) The ID of the playlist (program). + * - add (int, required) Use 1 to add the video to the playlist or 0 to remove it. + * + * Notes: + * - The user must be logged in. + * - The PlayLists plugin must be enabled. + * - The user must be the owner of the playlist. + * - The user must have permission to add the video to the playlist. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=11&playlists_id=10&add=1 + * + * @return \ApiObject Success or error status. + */ + /** + * @OA\Post( + * path="/api/programs", + * summary="Set Programs", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_programs($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/playlist.php'; + $users_id = User::getId(); + if (empty($users_id)) { + return new ApiObject("You must login first"); + } + $array = array('videos_id', 'playlists_id'); + foreach ($array as $value) { + if (empty($parameters[$value])) { + return new ApiObject("{$value} cannot be empty"); + } + } + + $obj = new stdClass(); + $obj->error = true; + $obj->status = 0; + + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin not enabled"); + } + + if (!PlayLists::canAddVideoOnPlaylist($parameters['videos_id'])) { + return new ApiObject("You can not add this video on playlist"); + } + + $playList = new PlayList($parameters['playlists_id']); + if (empty($playList) || User::getId() !== $playList->getUsers_id() || empty($parameters['videos_id'])) { + return new ApiObject("Permission denied"); + } + + $obj->error = false; + $obj->status = $playList->addVideo($parameters['videos_id'], $parameters['add']); + + return new ApiObject("", false, $obj); + } + + /** + * Retrieve all subscribers for a specific user. + * + * @param array $parameters + * - users_id (int, required) The user ID to retrieve the subscribers for. + * - APISecret (string, required) A valid API secret to authenticate the request. + * + * Notes: + * - Caches the result for 1 hour (3600 seconds). + * - Requires a valid APISecret. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?users_id=1&APIName={APIName}&APISecret={APISecret} + * + * @return \ApiObject A list of subscribers or an error object. + */ + /** + * @OA\Get( + * path="/api/subscribers", + * summary="Get Subscribers", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_subscribers($parameters) + { + global $global; + + $name = "get_api_subscribers" . json_encode($parameters); + $subscribers = ObjectYPT::getCacheGlobal($name, 3600); + if (empty($subscribers)) { + $obj = $this->startResponseObject($parameters); + if (self::isAPISecretValid()) { + return new ApiObject("Invalid APISecret"); + } + if (empty($parameters['users_id'])) { + return new ApiObject("User ID can not be empty"); + } + require_once $global['systemRootPath'] . 'objects/subscribe.php'; + $subscribers = Subscribe::getAllSubscribes($parameters['users_id']); + ObjectYPT::setCacheGlobal($name, $subscribers); + } + return new ApiObject("", false, $subscribers); + } + + /** + * Retrieve all categories available on the site. + * + * @param array $parameters + * (No parameters are required for this request) + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject A list of all site categories with basic information such as ID, icon class, name, and totals. + */ + /** + * @OA\Get( + * path="/api/categories", + * summary="Get Categories", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_categories($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/category.php'; + $categories = Category::getAllCategories(); + array_multisort(array_column($categories, 'hierarchyAndName'), SORT_ASC, $categories); + $list = []; + foreach ($categories as $value) { + $obj = new stdClass(); + $obj->id = $value['id']; + $obj->iconClass = $value['iconClass']; + $obj->hierarchyAndName = $value['hierarchyAndName']; + $obj->name = $value['name']; + $obj->clean_name = $value['clean_name']; + $obj->fullTotal = $value['fullTotal']; + $obj->total = $value['total']; + $list[] = $obj; + } + return new ApiObject("", false, $list); + } + + /** + * Retrieve the number of likes for a specific video. + * + * @param array $parameters + * @param int $parameters['videos_id'] (required) The ID of the video for which you want to retrieve the like count. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1 + * + * @return \ApiObject An object containing the total number of likes for the specified video. + */ + /** + * @OA\Get( + * path="/api/likes", + * summary="Get Likes", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_likes($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/like.php'; + if (empty($parameters['videos_id'])) { + return new ApiObject("Videos ID can not be empty"); + } + return new ApiObject("", false, Like::getLikes($parameters['videos_id'])); + } + + /** + * Register a "like" for a specific video from a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to like. + * @param string $parameters['user'] The username of the user performing the like action. + * @param string $parameters['pass'] The password of the user. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 + * + * @return \ApiObject Result of the like operation. + */ + /** + * @OA\Post( + * path="/api/like", + * summary="Set Like", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_like($parameters) + { + return $this->like($parameters, 1); + } + + /** + * Register a "dislike" for a specific video from a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to dislike. + * @param string $parameters['user'] The username of the user performing the dislike action. + * @param string $parameters['pass'] The password of the user. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 + * + * @return \ApiObject Result of the dislike operation. + */ + /** + * @OA\Post( + * path="/api/dislike", + * summary="Set Dislike", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_dislike($parameters) + { + return $this->like($parameters, -1); + } + + /** + * Remove a previously registered like/dislike from a specific video by a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to remove the like/dislike. + * @param string $parameters['user'] The username of the user performing the action. + * @param string $parameters['pass'] The password of the user. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 + * + * @return \ApiObject Result of the like removal operation. + */ + /** + * @OA\Post( + * path="/api/removelike", + * summary="Set Removelike", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_removelike($parameters) + { + return $this->like($parameters, 0); + } + + /** + * Authenticate a user and return a session token or error. + * + * @param array $parameters + * @param string $parameters['user'] The username of the user. + * @param string $parameters['pass'] The user's password (either raw or encrypted). + * @param bool [$parameters['encodedPass']] Optional. Set to true if the password is already encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON encoded authentication response or error message. + */ + /** + * @OA\Get( + * path="/api/signIn", + * summary="Get Signin", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_signIn($parameters) + { + global $global; + $this->getToPost(); + require_once $global['systemRootPath'] . 'objects/login.json.php'; + exit; + } + + /** + * Register a new user on the platform. + * + * @param array $parameters + * @param string $parameters['user'] The desired username of the user. + * @param string $parameters['pass'] The password for the user. + * @param string $parameters['email'] The email address of the user. + * @param string $parameters['name'] The full name of the user. + * @param int [$parameters['emailVerified']] Optional. Set to 1 if the user's email is already verified. + * @param int [$parameters['canCreateMeet']] Optional. Set to 1 if the user is allowed to create meetings. + * @param int [$parameters['canStream']] Optional. Set to 1 if the user is allowed to start live streams. + * @param int [$parameters['canUpload']] Optional. Set to 1 if the user is allowed to upload videos. + * @param string $parameters['APISecret'] Required. Secret key to authorize the API request. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&user=admin&pass=123&email=me@mysite.com&name=Yeshua + * + * @return string JSON encoded success or error response. + */ + /** + * @OA\Post( + * path="/api/signUp", + * summary="Set Signup", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_signUp($parameters) + { + global $global; + $this->getToPost(); + $obj = $this->getDataObject(); + if ($obj->APISecret !== @$_GET['APISecret']) { + return new ApiObject("APISecret Not valid"); + } + $ignoreCaptcha = 1; + if (isset($_REQUEST['emailVerified'])) { + $global['emailVerified'] = intval($_REQUEST['emailVerified']); + } + if (isset($_REQUEST['canCreateMeet'])) { + $global['canCreateMeet'] = intval($_REQUEST['canCreateMeet']); + } + if (isset($_REQUEST['canStream'])) { + $global['canStream'] = intval($_REQUEST['canStream']); + } + if (isset($_REQUEST['canUpload'])) { + $global['canUpload'] = intval($_REQUEST['canUpload']); + } + require_once $global['systemRootPath'] . 'objects/userCreate.json.php'; + exit; + } + + /** + * Handle like/dislike/removal actions for a video. + * + * @param array $parameters + * @param int $like The like value to apply: + * 1 = like, + * -1 = dislike, + * 0 = remove like/dislike. + * + * @param int $parameters['videos_id'] The ID of the video to apply the like action. + * + * @return \ApiObject The updated like status object or an error message. + */ + private function like($parameters, $like) + { + global $global; + require_once $global['systemRootPath'] . 'objects/like.php'; + if (empty($parameters['videos_id'])) { + return new ApiObject("Videos ID can not be empty"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + new Like($like, $parameters['videos_id']); + + $obj = Like::getLikes($parameters['videos_id']); + if (empty($obj)) { + $obj = new stdClass(); + } + + return new ApiObject("", false, $obj); + } + + /** + * Returns a VMAP (Video Multiple Ad Playlist) XML response for ads playback. + * + * If no user credentials are provided, ads will always be shown. + * If valid user credentials are passed, the script will decide based on user permissions whether to show ads. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. The video ID used to calculate the ads. + * @param int [$parameters['optionalAdTagUrl']] Optional. A tag identifier (1, 2, 3, or 4) to select a different ad tag. Defaults to the default tag if not passed. + * @param string [$parameters['user']] Optional. Username to check permissions for ad display. + * @param string [$parameters['pass']] Optional. Password of the user. + * @param bool [$parameters['encodedPass']] Optional. Indicates if the password is encrypted. + * + * @example XML Response: + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=secret&encodedPass=true&optionalAdTagUrl=2 + * @example JSON Response: + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=secret&encodedPass=true&optionalAdTagUrl=2&json=1 + * + * @return string XML or JSON response depending on the request. + */ + /** + * @OA\Get( + * path="/api/vmap", + * summary="Get Vmap", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_vmap($parameters) + { + global $global; + $this->getToPost(); + header('Content-type: application/xml'); + if (AVideoPlugin::isEnabledByName('GoogleAds_IMA')) { + require_once $global['systemRootPath'] . 'plugin/GoogleAds_IMA/VMAP.php'; + } else if (AVideoPlugin::isEnabledByName('AD_Server')) { + require_once $global['systemRootPath'] . 'plugin/AD_Server/VMAP.php'; + } else if (AVideoPlugin::isEnabledByName('AdsForJesus')) { + $videos_id = getVideos_id(); + $url = AdsForJesus::getVMAPURL($videos_id); + if (!empty($url)) { + echo url_get_contents($url); + } + } else { + echo ' '; + echo ' '; + } + exit; + } + + /** + * Returns a VAST (Video Ad Serving Template) XML response for ad playback. + * + * If no user credentials are provided, ads will always be shown. + * If valid user credentials are passed, the system will determine whether ads should be shown to the user. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. The video ID used to determine ad eligibility. + * @param int [$parameters['optionalAdTagUrl']] Optional. Tag identifier (1, 2, 3, or 4) to select a different ad tag. Defaults to the default tag if not provided. + * @param string [$parameters['user']] Optional. Username of the user. + * @param string [$parameters['pass']] Optional. Password of the user. + * @param bool [$parameters['encodedPass']] Optional. If true, indicates that the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true&optionalAdTagUrl=2 + * + * @return string XML response (VAST format) + */ + /** + * @OA\Get( + * path="/api/vast", + * summary="Get Vast", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_vast($parameters) + { + global $global; + $this->getToPost(); + $vastOnly = 1; + require_once $global['systemRootPath'] . 'plugin/GoogleAds_IMA/VMAP.php'; + exit; + } + + /** + * Return the location based on the provided IP address. + * + * @param array $parameters + * @param string $parameters['APISecret'] Required. Secret key for API access. + * @param string $parameters['ip'] Required. The IP address to retrieve location data for. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&ip=2.20.147.123 + * + * @return \ApiObject Location data including country, city, latitude, longitude, etc., or an error message if not available. + */ + /** + * @OA\Get( + * path="/api/IP2Location", + * summary="Get Ip2location", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_IP2Location($parameters) + { + global $global; + $this->getToPost(); + $obj = $this->getDataObject(); + if ($obj->APISecret !== @$_GET['APISecret']) { + return new ApiObject("APISecret Not valid"); + } + if (AVideoPlugin::isEnabledByName("User_Location")) { + $row = IP2Location::getLocation($parameters['ip']); + if (!empty($row)) { + return new ApiObject("", false, $row); + } + } + return new ApiObject("IP2Location not working"); + exit; + } + + /** + * Return all favorite videos from a specific user. + * + * @param array $parameters + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON-encoded list of favorite playlists and videos. + */ + /** + * @OA\Get( + * path="/api/favorite", + * summary="Get Favorite", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_favorite($parameters) + { + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + $row = PlayList::getAllFromUser(User::getId(), false, 'favorite'); + foreach ($row as $key => $value) { + $row[$key] = cleanUpRowFromDatabase($row[$key]); + foreach ($value['videos'] as $key2 => $value2) { + if (!empty($row[$key]['videos'][$key2]['next_videos_id'])) { + unset($_POST['searchPhrase']); + $row[$key]['videos'][$key2]['next_video'] = Video::getVideo($row[$key]['videos'][$key2]['next_videos_id']); + } + $row[$key]['videos'][$key2]['videosURL'] = getVideosURL($row[$key]['videos'][$key2]['filename']); + $row[$key]['videos'][$key2] = cleanUpRowFromDatabase($row[$key]['videos'][$key2]); + } + } + header('Content-Type: application/json'); + echo json_encode($row); + exit; + } + + /** + * Add a video to the user's favorite playlist. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. ID of the video to add. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON result indicating success or failure. + */ + /** + * @OA\Post( + * path="/api/favorite", + * summary="Set Favorite", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_favorite($parameters) + { + $this->favorite($parameters, true); + } + + /** + * Remove a video from the user's favorite playlist. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. ID of the video to remove. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON result indicating success or failure. + */ + /** + * @OA\Post( + * path="/api/removeFavorite", + * summary="Set Removefavorite", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_removeFavorite($parameters) + { + $this->favorite($parameters, false); + } + + private function favorite($parameters, $add) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("Wrong user or password"); + } + $_REQUEST['videos_id'] = $parameters['videos_id']; + $_REQUEST['add'] = $add; + $_REQUEST['playlists_id'] = PlayLists::getFavoriteIdFromUser(User::getId()); + header('Content-Type: application/json'); + require_once $global['systemRootPath'] . 'objects/playListAddVideo.json.php'; + exit; + } + + /** + * Return all videos in the "watch later" playlist for a user. + * + * @param array $parameters + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON encoded list of videos marked as watch later. + */ + /** + * @OA\Get( + * path="/api/watch_later", + * summary="Get Watch later", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_watch_later($parameters) + { + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + $row = PlayList::getAllFromUser(User::getId(), false, 'watch_later'); + foreach ($row as $key => $value) { + unset($row[$key]['password']); + unset($row[$key]['recoverPass']); + foreach ($value['videos'] as $key2 => $value2) { + //$row[$key]['videos'][$key2] = Video::getVideo($value2['id']); + unset($row[$key]['videos'][$key2]['password']); + unset($row[$key]['videos'][$key2]['recoverPass']); + if (!empty($row[$key]['videos'][$key2]['next_videos_id'])) { + unset($_POST['searchPhrase']); + $row[$key]['videos'][$key2]['next_video'] = Video::getVideo($row[$key]['videos'][$key2]['next_videos_id']); + } + $row[$key]['videos'][$key2]['videosURL'] = getVideosURL($row[$key]['videos'][$key2]['filename']); + unset($row[$key]['videos'][$key2]['password']); + unset($row[$key]['videos'][$key2]['recoverPass']); + } + } + echo json_encode($row); + exit; + } + + /** + * Add a video to the user's "watch later" playlist. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. ID of the video to be added. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON encoded result message. + */ + /** + * @OA\Post( + * path="/api/watch_later", + * summary="Set Watch later", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_watch_later($parameters) + { + $this->watch_later($parameters, true); + } + + /** + * Remove a video from the user's "watch later" playlist. + * + * @param array $parameters + * @param int $parameters['videos_id'] Required. ID of the video to be removed. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * + * @return string JSON encoded result message. + */ + /** + * @OA\Post( + * path="/api/removeWatch_later", + * summary="Set Removewatch later", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_removeWatch_later($parameters) + { + $this->watch_later($parameters, false); + } + + private function watch_later($parameters, $add) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("Wrong user or password"); + } + $_POST['videos_id'] = $parameters['videos_id']; + $_POST['add'] = $add; + $_POST['playlists_id'] = PlayLists::getWatchLaterIdFromUser(User::getId()); + require_once $global['systemRootPath'] . 'objects/playListAddVideo.json.php'; + exit; + } + + /** + * Send a message using the Chat2 plugin. + * + * @param array $parameters + * @param string $parameters['message'] Required. The message content, must be URL encoded. + * @param int $parameters['users_id'] Optional. The ID of the user to send the message to. + * @param int $parameters['room_users_id'] Optional. The ID of the channel or room to send the message to. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Indicates whether the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&message=HelloWorld&users_id=2&room_users_id=4&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * @return string JSON encoded result message. + */ + /** + * @OA\Post( + * path="/api/chat2_message", + * summary="Set Chat2 message", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_chat2_message($parameters) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled("Chat2"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + $_POST['message'] = @$parameters['message']; + $_GET['users_id'] = @$parameters['users_id']; + $_GET['room_users_id'] = @$parameters['room_users_id']; + include $global['systemRootPath'] . 'plugin/Chat2/sendMessage.json.php'; + exit; + } + + /** + * Retrieve chat messages using the Chat2 plugin. + * + * @param array $parameters + * @param int $parameters['to_users_id'] Optional. User ID for whom the private messages are intended. + * @param int $parameters['lower_then_id'] Optional. Only messages with ID less than this value will be returned. + * @param int $parameters['greater_then_id'] Optional. Only messages with ID greater than this value will be returned. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Indicates whether the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&greater_then_id=88&lower_then_id=98&to_users_id=2&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * @return string JSON encoded chat messages. + */ + /** + * @OA\Get( + * path="/api/chat2_chat", + * summary="Get Chat2 chat", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_chat2_chat($parameters) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled("Chat2"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + $_GET['to_users_id'] = @$parameters['to_users_id']; + $_GET['lower_then_id'] = @$parameters['lower_then_id']; + + if (!empty($parameters['greater_then_id'])) { + if (empty($_SESSION['chatLog'])) { + $_SESSION['chatLog'] = []; + } + if (empty($_SESSION['chatLog'][$_GET['to_users_id']])) { + $_SESSION['chatLog'][$_GET['to_users_id']] = []; + } + $_SESSION['chatLog'][$_GET['to_users_id']][0]['id'] = $parameters['greater_then_id']; + } + + include $global['systemRootPath'] . 'plugin/Chat2/getChat.json.php'; + exit; + } + + /** + * Retrieve public chat messages from a specific room using the Chat2 plugin. + * + * @param array $parameters + * @param int $parameters['room_users_id'] Required. User ID of the room/channel from which to retrieve messages. + * @param int $parameters['lower_then_id'] Optional. Only messages with ID less than this will be returned. + * @param int $parameters['greater_then_id'] Optional. Only messages with ID greater than this will be returned. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Whether the password is encrypted. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&greater_then_id=88&lower_then_id=98&room_users_id=2&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * @return string JSON encoded chat messages from the specified room. + */ + /** + * @OA\Get( + * path="/api/chat2_room", + * summary="Get Chat2 room", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_chat2_room($parameters) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled("Chat2"); + if (empty($plugin)) { + return new ApiObject("Plugin disabled"); + } + if (!User::isLogged()) { + return new ApiObject("User must be logged"); + } + $_GET['room_users_id'] = @$parameters['room_users_id']; + $_GET['lower_then_id'] = @$parameters['lower_then_id']; + + if (!empty($parameters['greater_then_id'])) { + if (empty($_SESSION['chatLog'])) { + $_SESSION['chatLog'] = []; + } + if (empty($_SESSION['chatLog'][$_GET['to_users_id']])) { + $_SESSION['chatLog'][$_GET['to_users_id']] = []; + } + $_SESSION['chatLog'][$_GET['to_users_id']][0]['id'] = $parameters['greater_then_id']; + } + + include $global['systemRootPath'] . 'plugin/Chat2/getRoom.json.php'; + exit; + } + + public static function getAPISecret() + { + $obj = AVideoPlugin::getDataObject("API"); + return $obj->APISecret; + } + + /** + * Retrieve available locale translations on the site. + * + * @param array $parameters Not used in this request. + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} + * + * @return \ApiObject + * { + * "default": "en_US", // Default language configured in the system + * "options": ["en_US", "pt_BR"], // List of enabled language options + * "isRTL": false // Boolean indicating if the current language is RTL + * } + */ + /** + * @OA\Get( + * path="/api/locales", + * summary="Get Locales", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_locales($parameters) + { + global $global, $config; + $langs = new stdClass(); + $langs->default = $config->getLanguage(); + $langs->options = getEnabledLangs(); + $langs->isRTL = isRTL(); + return new ApiObject("", false, $langs); + } + + /** + * Retrieve translations for a specific language. + * + * @param array $parameters + * 'language' (required) ISO code of the language to retrieve translations for (e.g., 'cn' for Chinese). + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&language=cn + * + * @return \ApiObject + * Returns a PHP array `$t` containing key-value translation pairs for the requested language. + * + * Error handling: + * - Returns error if 'language' parameter is missing. + * - Returns error if the language file does not exist. + * - Returns error if the translation array `$t` is empty. + */ + /** + * @OA\Get( + * path="/api/locale", + * summary="Get Locale", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_locale($parameters) + { + global $global, $config; + $obj = $this->startResponseObject($parameters); + + if (empty($parameters['language'])) { + return new ApiObject("You must specify a language"); + } + $parameters['language'] = strtolower($parameters['language']); + $file = "{$global['systemRootPath']}locale/{$parameters['language']}.php"; + if (!file_exists("{$file}")) { + return new ApiObject("This language does not exists"); + } + include $file; + if (empty($t)) { + return new ApiObject("This language is empty"); + } + + return new ApiObject("", false, $t); + } + + /** + * Update user profile and/or background images using URLs. + * + * @param array $parameters + * - 'APISecret' (string, required): Secret key for API authentication. + * - 'user' (string, required): Username of the user to update. + * - 'backgroundImg' (string, optional): URL to the new background image. + * - 'profileImg' (string, optional): URL to the new profile image. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&user=admin + * &backgroundImg=https%3A%2F%2Fexample.com%2Fbackground.jpg&profileImg=https%3A%2F%2Fexample.com%2Fprofile.jpg + * + * @return \ApiObject + * Returns an object with updated image info or error messages. + * + * Notes: + * - Both image parameters are optional; if omitted, the corresponding image will not be updated. + * - Requires a valid API secret. + * - If user does not exist, returns an error. + */ + /** + * @OA\Post( + * path="/api/userImages", + * summary="Set Userimages", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_userImages($parameters) + { + global $global; + require_once $global['systemRootPath'] . 'objects/video.php'; + if (self::isAPISecretValid()) { + $user = new User("", $parameters['user'], false); + if (empty($user->getUser())) { + return new ApiObject("User Not defined"); + } + + // UPDATED USER + $updateUser = $user->updateUserImages($parameters); + + return new ApiObject("", false, $updateUser); + } else { + return new ApiObject("API Secret is not valid"); + } + } + + /** + * Returns the list of scheduled meetings for the authenticated user. + * + * @param array $parameters + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - 'encodedPass' (bool, optional): Indicates if the password is encrypted (default: false). + * - 'time' (string, optional): Meeting timeframe filter. Accepted values: [today|upcoming|past]. Default: today. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&time=past + * + * @return \ApiObject + * Returns an array of meetings with their details. If user is a moderator or has access, the room password is included. + * Returns a message if there are no meetings scheduled. + * + * Notes: + * - Requires the Meet plugin to be enabled. + * - Requires valid user login credentials. + * - Meeting room password is only included if the user has permission to view it. + */ + /** + * @OA\Get( + * path="/api/meet", + * summary="Get Meet", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_meet($parameters) + { + global $global; + $meet = AVideoPlugin::loadPluginIfEnabled('Meet'); + if ($meet) { + $time = 'today'; + + if (!empty($_REQUEST['time'])) { + $time = $_REQUEST['time']; + } + + $meets = Meet_schedule::getAllFromUsersId(User::getId(), $time, true, false); + + foreach ($meets as $key => $value) { + $RoomPassword = ''; + if (self::isAPISecretValid() || Meet::isModerator($value['id']) || Meet::canJoinMeet($value['id'])) { + $RoomPassword = $value['password']; + } + + $meets[$key] = cleanUpRowFromDatabase($value); + $meets[$key]['RoomPassword'] = $RoomPassword; + } + if (empty($meets)) { + $message = _('You do not have any meetings available. you should set one first'); + } else { + $message = ''; + } + //var_dump($meets); + return new ApiObject($message, false, $meets); + } else { + return new ApiObject("Meet Plugin disabled"); + } + exit; + } + + /** + * Create or delete a scheduled meeting (Meet plugin required). + * + * @param array $parameters + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - 'RoomTopic' (string, required): The title of the meeting. + * - ['id'] (int, optional): If provided, deletes the meeting with this ID. + * - ['starts'] (string, optional): Start datetime of the meeting (default is now). + * - ['status'] (string, optional): 'a' for active, 'i' for inactive. + * - ['public'] (int, optional): 2 = public, 1 = logged-in users only, 0 = specific user groups (default: 2). + * - ['userGroups'] (array, optional): Array of user group IDs. + * - ['RoomPasswordNew'] (string, optional): Meeting password. + * - ['encodedPass'] (bool, optional): If true, treats the password as already encoded. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true&RoomTopic=MyMeeting + * + * @return string + * - On success: returns meeting data or confirmation of deletion. + * - On error: returns descriptive message. + * + * Notes: + * - Requires Meet plugin enabled. + * - User must have permission to create meetings. + * - If 'id' is passed, the function will delete the meeting instead of creating one. + */ + /** + * @OA\Post( + * path="/api/meet", + * summary="Set Meet", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_meet($parameters) + { + global $global; + $meet = AVideoPlugin::loadPluginIfEnabled('Meet'); + if ($meet) { + if (!User::canCreateMeet()) { + return new ApiObject("You cannot create a meet"); + } else { + include $global['systemRootPath'] . 'plugin/Meet/saveMeet.json.php'; + exit; + } + } else { + return new ApiObject("Meet Plugin disabled"); + } + exit; + } + + /** + * Return user notifications and current live streaming stats. + * + * @param array $parameters + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - ['encodedPass'] (bool, optional): If true, indicates the password is already encrypted. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true + * + * @return string + * - JSON-encoded response containing: + * - notifications (from UserNotifications plugin) + * - live (from Live plugin stats) + * + * Notes: + * - Requires the **UserNotifications** plugin enabled. + * - Also fetches live stream stats via `Live/stats.json.php`. + * - Returns an error if the plugin is not enabled. + */ + /** + * @OA\Get( + * path="/api/notifications", + * summary="Get Notifications", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_notifications($parameters) + { + global $global; + $plugin = AVideoPlugin::loadPluginIfEnabled('UserNotifications'); + if ($plugin) { + $url = "{$global['webSiteRootURL']}plugin/UserNotifications/getNotifications.json.php"; + $rows = json_decode(url_get_contents($url, "", 0, false, true)); + $url = "{$global['webSiteRootURL']}plugin/Live/stats.json.php"; + $live = json_decode(url_get_contents($url, "", 0, false, true)); + $rows->live = $live; + return new ApiObject('', false, $rows); + } else { + return new ApiObject("UserNotifications Plugin disabled"); + } + exit; + } + + + /** + * Return the Roku-compatible JSON feed for the app. + * + * @param array $parameters + * - 'APISecret' (string, required): Required to access all videos. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} + * + * @return \ApiObject + * - Returns an object with the following structure: + * - providerName: Website title + * - language: Feed language (default: 'en') + * - lastUpdated: ISO 8601 date string + * - Sections with video entries (from Gallery or YouPHPFlix2 plugin) + * - cache: Whether the response was newly cached + * - cached: Boolean indicating if the response was served from cache + * + * Notes: + * - Uses `YouPHPFlix2` plugin if enabled; falls back to `Gallery`. + * - Caches result globally for 1 hour. + * - Requires `rowToRoku()` function to format video rows. + */ + /** + * @OA\Get( + * path="/api/app", + * summary="Get App", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_app($parameters) + { + global $global, $config; + $name = "get_api_roku" . json_encode($parameters); + $roku = ObjectYPT::getCacheGlobal($name, 3600); + if (empty($roku)) { + if (AVideoPlugin::isEnabledByName("YouPHPFlix2")) { + $url = "{$global['webSiteRootURL']}plugin/API/get.json.php?APIPlugin=YouPHPFlix2&APIName=firstPage"; + } else { + $url = "{$global['webSiteRootURL']}plugin/API/get.json.php?APIPlugin=Gallery&APIName=firstPage"; + } + $content = url_get_contents_with_cache($url); + //$content = url_get_contents($url); + $json = _json_decode($content); + + $roku = new stdClass(); + $roku->providerName = $config->getWebSiteTitle(); + $roku->language = "en"; + $roku->lastUpdated = date('c'); + foreach ($json->response->sections as $section) { + $array = array(); + //var_dump($section->endpointResponse); + if (!empty($section->endpointResponse->rows)) { + foreach ($section->endpointResponse->rows as $row) { + $movie = rowToRoku($row); + if (!empty($movie)) { + $array[] = $movie; + } + } + } + if (!empty($array)) { + $roku->{$section->title} = $array; + } + } + //var_dump($roku);exit; + $roku->cache = ObjectYPT::setCacheGlobal($name, $roku); + $roku->cached = false; + } else { + $roku->cached = true; + } + return new ApiObject("", false, $roku); + } + + /** + * @param array $parameters + * + * Generates a one-time login code for a specific user. + * + * Description: + * - Accepts a username and password. + * - Generates a unique login code (one-time use). + * - Stores the encrypted code and user data in a log file. + * - The code expires after 10 minutes. + * + * Parameters: + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b + * + * @return \ApiObject + * - success: true if code generated + * - response: contains 'code', 'user', 'bytes' and other encrypted user data + */ + /** + * @OA\Post( + * path="/api/login_code", + * summary="Set Login code", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_login_code($parameters) + { + $obj = getActivationCode(); + return new ApiObject('', empty($obj['bytes']), $obj); + } + + /** + * @param array $parameters + * + * Verifies a one-time login code. + * + * Description: + * - Takes a one-time login code as input. + * - Attempts to locate and decrypt the corresponding log file. + * - If valid, returns the user’s data. + * - The login code is valid for a limited time and is deleted after use. + * + * Parameters: + * - 'code' (string, required): The login code previously generated by `set_api_login_code`. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&code=XXXX-XXXX + * + * @return \ApiObject + * - success: true if the code is valid and not expired + * - response: object containing user ID, name, email, photo URL, and user hash + * - error: returns appropriate message if the code is invalid, expired, not found, or corrupted + */ + /** + * @OA\Get( + * path="/api/login_code", + * summary="Get Login code", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_login_code($parameters) + { + global $global, $config; + $msg = ''; + $obj = false; + if (!empty($parameters['code'])) { + $path = getTmpDir('loginCodes'); + $filename = "{$path}{$parameters['code']}.log"; + if (file_exists($filename)) { + $content = file_get_contents($filename); + unlink($filename); + $string = decryptString($content); + if (!empty($string)) { + $obj = json_decode($string); + if ($obj->expires < time()) { + $msg = 'Code is expired'; + $obj = false; + } else { + $obj->photo = User::getPhoto($obj->users_id); + $obj->identification = User::getNameIdentificationById($obj->users_id); + $obj->email = User::getEmailDb($obj->users_id); + $obj->passhash = User::getUserHash($obj->users_id, $valid = '+1 year'); + } + } else { + $msg = 'Code is corrupted'; + } + } else { + $msg = 'Code not found'; + } + } else { + $msg = 'You need to provide a code'; + } + + return new ApiObject($msg, empty($obj), $obj); + } + + + /** + * @param array $parameters + * + * Updates the birth date of the currently logged-in user. + * + * Required Parameters: + * - 'birth_date' (string): The user's birth date in Y-m-d format (e.g., "1997-06-17"). + * + * Optional Authentication: + * - 'user' (string): Username for authentication (if not already logged in). + * - 'pass' (string): Password for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&birth_date=1997-06-17 + * + * @return \ApiObject + * - success: true if the birth date is updated successfully. + * - response: object containing the `users_id` and `error` flag. + * - error: true if the user is not logged in or update fails. + */ + /** + * @OA\Post( + * path="/api/birth", + * summary="Set Birth", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_birth($parameters) + { + global $global; + $users_id = User::getId(); + if (empty($users_id)) { + return new ApiObject("You must login first"); + } + $msg = ''; + $obj = new stdClass(); + + $user = new User(0); + $user->loadSelfUser(); + $user->setBirth_date($_REQUEST['birth_date']); + $obj->users_id = $user->save(); + $obj->error = empty($obj->users_id); + User::updateSessionInfo(); + + return new ApiObject($msg, $obj->error, $obj); + } + + /** + * @param array $parameters + * + * Check if a user's email is verified. + * + * Required Parameters: + * - 'users_id' (int): The ID of the user you want to check. + * - 'APISecret' (string): Required for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - response: object with: + * - users_id: The user ID provided + * - email_verified: true if the user's email is verified, false otherwise + * - error: true if the APISecret or users_id is invalid or missing + */ + /** + * @OA\Get( + * path="/api/is_verified", + * summary="Get Is verified", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_is_verified($parameters) + { + global $global; + if (!self::isAPISecretValid()) { + return new ApiObject("APISecret is required"); + } + $obj = new stdClass(); + $obj->users_id = intval($_REQUEST['users_id']); + if (empty($obj->users_id)) { + return new ApiObject("Users ID is required"); + } + $user = new User($obj->users_id); + $obj->email_verified = !empty($user->getEmailVerified()); + + return new ApiObject('', false, $obj); + } + + /** + * @param array $parameters + * + * Send a verification email to the specified user. + * + * Required Parameters: + * - 'users_id' (int): The ID of the user to whom the verification email should be sent. + * - 'APISecret' (string): Required for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - response: object with: + * - users_id: The user ID provided + * - sent: true if the email was sent successfully, false otherwise + * - error: true if APISecret is invalid or users_id is missing + */ + /** + * @OA\Post( + * path="/api/send_verification_email", + * summary="Set Send verification email", + * tags={"API"}, + * @OA\RequestBody( + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(type="object") + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation" + * ) + * ) + */ + public function set_api_send_verification_email($parameters) + { + global $global; + if (!self::isAPISecretValid()) { + return new ApiObject("APISecret is required"); + } + $obj = new stdClass(); + $obj->users_id = intval($_REQUEST['users_id']); + if (empty($obj->users_id)) { + return new ApiObject("Users ID is required"); + } + $user = new User($obj->users_id); + $obj->sent = User::sendVerificationLink($obj->users_id); + return new ApiObject('', false, $obj); + } + + public static function isAPISecretValid() + { + global $global; + if (!empty($_REQUEST['APISecret'])) { + $dataObj = AVideoPlugin::getDataObject('API'); + if (trim($dataObj->APISecret) === trim($_REQUEST['APISecret'])) { + $global['bypassSameDomainCheck'] = 1; + return true; + } + } + return false; + } + + /** + * Check if the provided APISecret is valid. + * + * Required Parameters: + * - 'APISecret' (string): The secret key to validate. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - message: "APISecret is valid" or "APISecret is invalid" + * - error: false if valid, true if invalid + */ + /** + * @OA\Get( + * path="/api/isAPISecretValid", + * summary="Get Isapisecretvalid", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_isAPISecretValid() + { + global $global; + if (!self::isAPISecretValid()) { + return new ApiObject("APISecret is invalid"); + } else { + return new ApiObject("APISecret is valid", false); + } + } + + /** + * @OA\Get( + * path="/api/decrypt", + * summary="Decrypt a string encrypted by the system", + * tags={"API"}, + * @OA\Parameter( + * name="APIName", + * in="query", + * required=true, + * @OA\Schema(type="string"), + * description="The name of the API to call" + * ), + * @OA\Parameter( + * name="string", + * in="query", + * required=true, + * @OA\Schema(type="string"), + * description="The encrypted string to decrypt" + * ), + * @OA\Response( + * response=200, + * description="Decrypted string", + * @OA\JsonContent(ref="#/components/schemas/ApiObject") + * ) + * ) + */ + /** + * @OA\Get( + * path="/api/decryptString", + * summary="Get Decryptstring", + * tags={"API"}, + * @OA\Parameter( + * name="parameters", + * in="query", + * required=false, + * @OA\Schema(type="object"), + * description="Optional parameters" + * ), + * @OA\Response( + * response=200, + * description="Successful response" + * ) + * ) + */ + public function get_api_decryptString() + { + $string = decryptString($_REQUEST['string']); + return new ApiObject($string, empty($string)); + } +} + +class ApiObject +{ + + public $error; + public $message; + public $response; + public $msg; + public $users_id; + public $user_age; + public $session_id; + + public function __construct($message = "api not started or not found", $error = true, $response = []) + { + $response = cleanUpRowFromDatabase($response); + + $this->error = $error; + $this->msg = $message; + $this->message = $message; + $this->response = $response; + $this->users_id = User::getId(); + $this->user_age = User::getAge(); + $this->session_id = session_id(); + } +} + +class SectionFirstPage +{ + + public $type; + public $title; + public $endpoint; + public $nextEndpoint; + public $rowCount; + public $endpointResponse; + public $totalRows; + public $childs; + public $executionTime; + + // Add constructor, getter, and setter here + public function __construct($type, $title, $endpoint, $rowCount, $childs = array()) + { + global $global; + $endpoint = addQueryStringParameter($endpoint, 'current', 1); + $endpoint = addQueryStringParameter($endpoint, 'videoType', 'audio_and_video_and_serie'); + $endpoint = addQueryStringParameter($endpoint, 'noRelated', 1); + $this->type = $type; + $this->title = $title; + $this->endpoint = $endpoint; + $this->nextEndpoint = addQueryStringParameter($endpoint, 'current', 2); + $this->rowCount = $rowCount; + $endpointURL = addQueryStringParameter($endpoint, 'rowCount', $rowCount); + if (User::isLogged()) { + + $endpointURL = addQueryStringParameter($endpointURL, 'user', User::getUserName()); + $endpointURL = addQueryStringParameter($endpointURL, 'pass', User::getUserPass()); + $endpointURL = addQueryStringParameter($endpointURL, 'webSiteRootURL', $global['webSiteRootURL']); + + //$endpointURL = addQueryStringParameter($endpointURL, 'PHPSESSID', session_id()); + } + $start = microtime(true); + //$endPointResponse = url_get_contents($endpointURL, '', 5, false, true); + $endPointResponse = url_get_contents_with_cache($endpointURL, 300, '', 5, false, true); + $this->executionTime = microtime(true) - $start; + //_error_log(gettype($endPointResponse).' '.json_encode($endPointResponse)); + if (!empty($endPointResponse)) { + if (is_string($endPointResponse)) { + $response = json_decode($endPointResponse); + } else { + $response = $endPointResponse; + } + /* + if(User::isLogged()){ + session_id($response->session_id); + } + */ + if (!empty($response)) { + $this->endpointResponse = $response->response; + $this->totalRows = $this->endpointResponse->totalRows; + } else { + $this->endpointResponse = new stdClass(); + $this->totalRows = 0; + } + } + $this->childs = $childs; + } +} diff --git a/plugin/API/API.php b/plugin/API/API.php index b1b64aaa8d..46384f0030 100644 --- a/plugin/API/API.php +++ b/plugin/API/API.php @@ -1,8 +1,29 @@ []] ], + components: new OA\Components( + securitySchemes: [ + 'APISecret' => new OA\SecurityScheme( + type: 'http', + scheme: 'bearer', + bearerFormat: 'API key', + securityScheme: 'APISecret', + description: 'Your API secret to authorize access' + ) + ] + ) +)] class API extends PluginAbstract { @@ -168,12 +189,39 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'plugin_name' The plugin name that you want to retrieve the parameters - * 'APISecret' to list all videos + * Retrieves the configuration parameters of a given plugin. + * + * @param array $parameters { + * @type string $plugin_name Required. The name of the plugin to retrieve the parameters for. + * @type string $APISecret Required. A valid API secret to authorize the request. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&rowCount=3&APISecret={APISecret} + * * @return \ApiObject */ + #[OA\Get( + path: "/api/plugin_parameters", + summary: "Retrieves the configuration parameters of a given plugin", + tags: ["API"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "plugin_name", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The name of the plugin to retrieve the parameters for" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns plugin configuration parameters" + ) + ] + )] public function get_api_plugin_parameters($parameters) { global $global; @@ -198,14 +246,51 @@ class API extends PluginAbstract /** * Get ads information for a specific video, user, or live stream. * - * @param array $parameters - * 'users_id' (optional) The users_id for which you want to retrieve ads information. If provided, - * ads specific to the user will be retrieved. If no ads are set for the user, global ads will be returned. - * 'videos_id' (optional) The videos_id for which you want to retrieve the users_id. - * 'live_key' (optional) The live_key for which you want to retrieve the users_id + * @param array $parameters { + * @type int|null $users_id Optional. The user ID to retrieve specific ads. Falls back to global ads if none found. + * @type int|null $videos_id Optional. If provided, the owner of the video will be used to retrieve ads. + * @type string|null $live_key Optional. If provided and the Live plugin is enabled, the user ID will be resolved by the live stream key. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1 - * @return array|ApiObject An array containing the ads information or an ApiObject with an error message. + * + * @return array|\ApiObject An array with ads data or an ApiObject containing an error message. */ + #[OA\Get( + path: "/api/adsInfo", + summary: "Get ads information for a specific video, user, or live stream", + tags: ["Ads"], + parameters: [ + new OA\Parameter( + name: "users_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "The user ID to retrieve specific ads. Falls back to global ads if none found" + ), + new OA\Parameter( + name: "videos_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "The video ID to resolve the video owner and retrieve ads accordingly" + ), + new OA\Parameter( + name: "live_key", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Live stream key used to resolve user ID if the Live plugin is enabled" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns ad configuration for the identified context" + ) + ] + )] public function get_api_adsInfo($parameters) { @@ -263,32 +348,81 @@ class API extends PluginAbstract /** - * @param array $parameters - * 'APISecret' also tell if the APISecret is valid or not - * Returns the site unique ID - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * Detects if the request is coming from a mobile device based on User-Agent and headers. + * + * @param array $parameters { + * @type string $userAgent Required. Typically the value of $_SERVER["HTTP_USER_AGENT"]. + * @type string|array $httpHeaders Optional. Typically the value of getallheaders() or $_SERVER["HTTP_X_REQUESTED_WITH"]. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=Mozilla%2F5.0+... + * + * @return \ApiObject Object with detection result and debug information. */ + #[OA\Get( + path: "/api/id", + summary: "Get the platform unique ID and validate the API secret", + tags: ["API"], + security: [ ['APISecret' => []] ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns the platform ID and whether the API secret is valid" + ) + ] + )] public function get_api_id($parameters) { + $apiResponse = new ApiObject(); global $global; $obj = $this->startResponseObject($parameters); $obj->id = getPlatformId(); $obj->isAPISecretValid = self::isAPISecretValid(); - return new ApiObject("", false, $obj); + return $apiResponse->finalize("", false, $obj); } /** - * @param array $parameters - * This will check if the provided UserAgent/Headers comes from a mobile - * Returns true if any type of mobile device detected, including special ones - * PHP Sample code: "plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=".urlencode($_SERVER["HTTP_USER_AGENT"])."&httpHeaders=".urlencode(json_encode(getallheaders())) - * ['userAgent' usually is the variable $_SERVER["HTTP_USER_AGENT"]] - * ['httpHeaders' usually is the variable $_SERVER['HTTP_X_REQUESTED_WITH']] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F89.0.4389.82+Safari%2F537.36 - * @return \ApiObject + * Checks if the request is coming from a mobile device based on User-Agent and HTTP headers. + * + * @param array $parameters { + * @type string $userAgent Required. Typically the value of $_SERVER["HTTP_USER_AGENT"]. + * @type string|array|null $httpHeaders Optional. Usually the value of json_encoded getallheaders() or $_SERVER["HTTP_X_REQUESTED_WITH"]. + * } + * + * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&userAgent=Mozilla%2F5.0... + * + * @return \ApiObject Object containing `userAgent`, `httpHeaders`, and a boolean `isMobile` flag. */ + #[OA\Get( + path: "/api/is_mobile", + summary: "Detect if the client device is a mobile device", + tags: ["API"], + parameters: [ + new OA\Parameter( + name: "userAgent", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "User-Agent string from the client request" + ), + new OA\Parameter( + name: "httpHeaders", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "HTTP headers (JSON string or single string) to assist in detection" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns whether the request is from a mobile device along with debugging info" + ) + ] + )] public function get_api_is_mobile($parameters) { global $global; @@ -308,16 +442,78 @@ class API extends PluginAbstract } /** - * @param array $parameters - * ['sort' database sort column] - * ['rowCount' max numbers of rows] - * ['current' current page] - * ['searchPhrase' to search on the categories] - * ['parentsOnly' will bring only the parents, not children categories] - * ['catName' the clean_Name of the category you want to filter] + * Retrieves categories with optional filters such as pagination, search, and specific category name. + * + * @param array $parameters { + * @type array $sort Optional. Sorting options, e.g., ['created' => 'DESC']. + * @type int|null $rowCount Optional. Maximum number of rows to return. + * @type int|null $current Optional. Current page number. + * @type string|null $searchPhrase Optional. Search keyword to filter categories. + * @type bool|null $parentsOnly Optional. If true, only parent categories will be returned. + * @type string|null $catName Optional. The `clean_name` of a specific category to retrieve. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&rowCount=3¤t=1&sort[created]=DESC - * @return \ApiObject + * + * @return \ApiObject Object containing filtered categories, metadata, and image information. */ + + #[OA\Get( + path: "/api/category", + summary: "Retrieves categories with optional filters such as pagination, search, and specific category name", + tags: ["API"], + parameters: [ + new OA\Parameter( + name: "sort", + in: "query", + required: false, + schema: new OA\Schema(type: "object"), + description: "Sorting options, e.g., sort[created]=DESC" + ), + new OA\Parameter( + name: "rowCount", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Maximum number of rows to return" + ), + new OA\Parameter( + name: "current", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Current page number" + ), + new OA\Parameter( + name: "searchPhrase", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Search keyword to filter categories" + ), + new OA\Parameter( + name: "parentsOnly", + in: "query", + required: false, + schema: new OA\Schema(type: "boolean"), + description: "If true, only parent categories will be returned" + ), + new OA\Parameter( + name: "catName", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "The clean_name of a specific category to retrieve" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns filtered list of categories with metadata and images" + ) + ] + )] public function get_api_category($parameters) { global $global; @@ -372,13 +568,47 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'APISecret' to list all videos - * 'playlists_id' the program id - * 'index' the position of the video + * Retrieves a specific video from a program (playlist) based on its index. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize the request. + * @type int|null $playlists_id Optional. The ID of the playlist (program) to retrieve the video from. + * @type int|null $index Optional. The index (position) of the video within the playlist. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=1&index=2&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing video details and channel metadata. */ + #[OA\Get( + path: "/api/video_from_program", + summary: "Retrieves a specific video from a playlist based on its index", + tags: ["Programs"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "playlists_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of the playlist (program) to retrieve the video from" + ), + new OA\Parameter( + name: "index", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "The position of the video within the playlist" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns the video data and related channel information" + ) + ] + )] public function get_api_video_from_program($parameters) { global $global; @@ -426,13 +656,47 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'APISecret' to list all videos - * 'playlists_id' the program id - * 'index' the position of the video + * Retrieves a specific audio track from a program (playlist) based on its index. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize the request. + * @type int|null $playlists_id Optional. The ID of the playlist (program) to retrieve the audio from. + * @type int|null $index Optional. The index (position) of the audio within the playlist. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=1&index=2&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing audio details and channel metadata. */ + #[OA\Get( + path: "/api/audio_from_program", + summary: "Retrieves a specific audio track from a playlist based on its index", + tags: ["Programs"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "playlists_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of the playlist (program) to retrieve the audio from" + ), + new OA\Parameter( + name: "index", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "The position of the audio track within the playlist" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns the audio data and related channel information" + ) + ] + )] public function get_api_audio_from_program($parameters) { $parameters['audioOnly'] = 1; @@ -440,10 +704,26 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Retrieves a list of suggested programs (playlists) including a default "Date Added" group. + * + * @param array $parameters Currently not used but reserved for future filtering or customization. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject Object containing a list of suggested playlists with channel and video information. */ + #[OA\Get( + path: "/api/suggested_programs", + summary: "Retrieves a list of suggested programs (playlists) including a default 'Date Added' group", + tags: ["Programs"], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns a list of suggested playlists with channel and video information" + ) + ] + )] public function get_api_suggested_programs($parameters) { global $global; @@ -482,12 +762,37 @@ class API extends PluginAbstract } /** - * This API will return all the tags from VideoTags plugin, also will list the latest 100 videos from the tags your user is subscribed to - * @param array $parameters - * 'audioOnly' 1 or 0, this option will extract the MP3 from the video file + * Returns all tags from the VideoTags plugin and lists the latest videos from the tags the user is subscribed to. + * + * @param array $parameters { + * @type int|bool|null $audioOnly Optional. If set to 1 or true, the response will include audio-only (MP3) versions of the videos. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject Object containing tag information and related videos, if subscribed. */ + #[OA\Get( + path: "/api/tags", + summary: "Returns all tags and optionally videos from the tags the user is subscribed to", + tags: ["Videos"], + parameters: [ + new OA\Parameter( + name: "audioOnly", + in: "query", + required: false, + schema: new OA\Schema(type: "boolean"), + description: "If true, returns only audio (MP3) versions of the videos" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns tag list and videos if user is subscribed" + ) + ] + )] public function get_api_tags($parameters) { global $global; @@ -518,12 +823,39 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'APISecret' to list all videos - * 'videos_id' the video id + * Returns detailed information and file sources for a specific video. + * + * @param array $parameters { + * @type string $APISecret Required. A valid API secret to authorize access to video details or list all the videos. + * @type int $videos_id Required. The ID of the video to retrieve. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing video metadata, duration, file path, sources, and images. */ + #[OA\Get( + path: "/api/video_file", + summary: "Returns detailed information and file sources for a specific video", + tags: ["Videos"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video to retrieve" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns video metadata, file paths, and source URLs" + ) + ] + )] public function get_api_video_file($parameters) { global $global; @@ -545,13 +877,45 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'videos_id' the video id - * 'users_id' the user id - * Returns if the user can watch the video + * Checks if a specific user can watch a given video, with or without ads. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to check. + * @type int $users_id Required. The ID of the user to verify access for. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject Object containing access permission flags for the specified user and video. */ + #[OA\Get( + path: "/api/user_can_watch_video", + summary: "Check if a user is allowed to watch a specific video", + tags: ["Videos", "Users"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video to check access for" + ), + new OA\Parameter( + name: "users_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the user whose access is being checked" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns access status flags for the given user and video" + ) + ] + )] public function get_api_user_can_watch_video($parameters) { @@ -577,13 +941,46 @@ class API extends PluginAbstract /** - * @param array $parameters - * 'videos_id' the video id - * 'password' a string with the user password - * Returns if the password is correct or not, if there is no password it will return true + * Verifies whether the provided password is correct for a given video. + * If the video has no password, it will return true. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to check. + * @type string $video_password Required. The password provided by the user. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject Object indicating whether the password is correct for the video. */ + #[OA\Get( + path: "/api/video_password_is_correct", + summary: "Verify if a password is correct for a specific video", + tags: ["Videos"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video to check password against" + ), + new OA\Parameter( + name: "video_password", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The password provided by the user to access the video" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns true if the password is correct or if the video has no password" + ) + ] + )] public function get_api_video_password_is_correct($parameters) { @@ -609,12 +1006,37 @@ class API extends PluginAbstract } /** - * @param array $parameters - * videos_id - * Returns the payperview plans + * Retrieves all Pay-Per-View (PPV) plans associated with a specific video. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve PPV plans for. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2 - * @return \ApiObject + * + * @return \ApiObject Object containing the list of PPV plans and formatted pricing. */ + #[OA\Get( + path: "/api/ppv_plans", + summary: "Retrieve Pay-Per-View (PPV) plans for a specific video", + tags: ["Monetization"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video to retrieve associated PPV plans" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns a list of PPV plans with formatted pricing" + ) + ] + )] public function get_api_ppv_plans($parameters) { global $global; @@ -649,15 +1071,62 @@ class API extends PluginAbstract } /** - * @param array $parameters - * reduces the wallet balance of a user by the cost of a pay-per-view (PPV) video and returns the updated balance. It checks if the user has sufficient funds to make the purchase - * plans_id - * videos_id - * 'user' username of the user - * 'pass' password of the user + * Processes the purchase of a Pay-Per-View (PPV) plan for a video and deducts the value from the user's wallet. + * Verifies if the user is logged in and has sufficient balance, then returns the updated plan status and wallet info. + * + * @param array $parameters { + * @type int $plans_id Required. The ID of the PPV plan to be purchased. + * @type int $videos_id Required. The ID of the video to be unlocked. + * @type string $user Optional. Username of the user (used for authentication if session is not set). + * @type string $pass Optional. Password of the user (used for authentication if session is not set). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2&plans_id=4 - * @return \ApiObject + * + * @return \ApiObject Object with purchase result, user info, and plan details. */ + #[OA\Post( + path: "/api/ppv_buy", + summary: "Processes the purchase of a PPV plan and deducts from the user's wallet", + tags: ["Monetization"], + parameters: [ + new OA\Parameter( + name: "plans_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the PPV plan to be purchased" + ), + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video to be unlocked" + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Username for login (optional if session exists)" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password for login (optional if session exists)" + ), + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns result of the purchase and wallet info" + ) + ] + )] public function set_api_ppv_buy($parameters) { global $global; @@ -706,12 +1175,38 @@ class API extends PluginAbstract } /** - * @param array $parameters - * videos_id - * Returns the payperview plans + * Retrieves all subscription plans associated with a specific video. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve subscription plans for. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=2 - * @return \ApiObject + * + * @return \ApiObject Object containing the list of available subscription plans for the video. */ + #[OA\Get( + path: "/api/subscription_plans", + summary: "Get subscription plans for a specific video", + tags: ["Monetization"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video to retrieve subscription plans" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response" + ) + ] + )] + public function get_api_subscription_plans($parameters) { global $global; @@ -743,15 +1238,64 @@ class API extends PluginAbstract } /** - * @param array $parameters - * reduces the wallet balance of a user by the cost of a pay-per-view (PPV) video and returns the updated balance. It checks if the user has sufficient funds to make the purchase - * plans_id - * videos_id - * 'user' username of the user - * 'pass' password of the user + * Processes the purchase of a subscription plan for a video and deducts the value from the user's wallet. + * Verifies if the user is logged in and has sufficient balance, then returns the subscription status. + * + * @param array $parameters { + * @type int $plans_id Required. The ID of the subscription plan to be purchased. + * @type int $videos_id Required. The ID of the video to associate with the subscription. + * @type string $user Optional. Username of the user (used for authentication if session is not set). + * @type string $pass Optional. Password of the user (used for authentication if session is not set). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&plans_id=2 - * @return \ApiObject + * + * @return \ApiObject Object with subscription result, user info, and plan details. */ + #[OA\Post( + path: "/api/subscription_buy", + summary: "Purchase a subscription plan for a video", + tags: ["Monetization"], + parameters: [ + new OA\Parameter( + name: "plans_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the subscription plan to be purchased" + ), + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video associated with the subscription" + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Username used for authentication (optional if session exists)" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password used for authentication (optional if session exists)" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns subscription result, user info, and plan details" + ) + ] + )] + + public function set_api_subscription_buy($parameters) { global $global; @@ -800,31 +1344,166 @@ class API extends PluginAbstract } /** - * @param array $parameters - * Obs: in the Trending sort also pass the current=1, otherwise it will return a random order + * Retrieves a list of videos with advanced filtering and sorting options. + * Supports filtering by category, tag, user/channel, type, and more. + * Includes pagination, playlist-compatible format, subtitles, comments, related videos, and advertising data. * - * ['APISecret' to list all videos] - * ['sort' database sort column] - * ['videos_id' the video id (will return only 1 or 0 video)] - * ['clean_title' the video clean title (will return only 1 or 0 video)] - * ['rowCount' max numbers of rows] - * ['current' current page] - * ['searchPhrase' to search on the categories] - * ['tags_id' the ID of the tag you want to filter] - * ['catName' the clean_name of the category you want to filter, the value can be an array of categories clean titles] - * ['doNotShowCats' the clean_name of the category you want to exclude from the list, the value can be an array of categories clean titles] - * ['channelName' the channelName of the videos you want to filter] - * ['playlist' use playlist=1 to get a response compatible with the playlist endpoint] - * ['videoType' the type of the video, the valid options are 'audio_and_video_and_serie', 'audio_and_video', 'audio', 'video', 'embed', 'linkVideo', 'linkAudio', 'torrent', 'pdf', 'image', 'gallery', 'article', 'serie', 'image', 'zip', 'notfound', 'blockedUser'] - * ['is_serie' if is 0 return only videos, if is 1 return only series, if is not set, return all] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&catName=default&rowCount=10 - * @example Suggested ----> {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[suggested]=1 - * @example DateAdded ----> {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[created]=desc - * @example Trending ----> {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[trending]=1 - * @example Shorts ----> {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[shorts]=1 - * @example MostWatched ----> {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[views_count]=desc - * @return \ApiObject + * @param array $parameters { + * @type string $APISecret Optional. If provided and valid, returns all videos regardless of restrictions. + * @type array|string $sort Optional. Sorting options (e.g., sort[created]=desc, sort[trending]=1). + * @type int $videos_id Optional. The ID of a specific video to retrieve. + * @type string $clean_title Optional. The clean title of a specific video to retrieve. + * @type int $rowCount Optional. Maximum number of rows to return (for pagination). + * @type int $current Optional. Current page number (required for 'trending' sort to work properly). + * @type string $searchPhrase Optional. Search phrase to filter categories. + * @type int $tags_id Optional. ID of a tag to filter videos by. + * @type string|array $catName Optional. Clean name(s) of category(ies) to include. + * @type string|array $doNotShowCats Optional. Clean name(s) of category(ies) to exclude. + * @type string $channelName Optional. Filter videos by channel name. + * @type int|bool $playlist Optional. If set to 1, returns data in playlist-compatible format. + * @type string $videoType Optional. Type of video (e.g., 'video', 'audio', 'serie', 'embed', etc.). + * @type int|bool $is_serie Optional. 0 to return only videos, 1 for series, unset for all. + * @type int|bool $noRelated Optional. If set, disables fetching related videos. + * } + * + * @example {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&catName=default&rowCount=10 + * @example Suggested → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[suggested]=1 + * @example DateAdded → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[created]=desc + * @example Trending → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[trending]=1¤t=1 + * @example Shorts → {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[shorts]=1 + * @example MostWatched→ {webSiteRootURL}plugin/API/get.json.php?APIName={APIName}&rowCount=10&sort[views_count]=desc + * + * @return \ApiObject Object containing video rows, metadata, and related details such as comments, subtitles, ads, and images. */ + #[OA\Get( + path: "/api/video", + summary: "Retrieve a list of videos with advanced filtering and sorting", + description: << []] ], + parameters: [ + new OA\Parameter( + name: "sort", + in: "query", + required: false, + schema: new OA\Schema(type: "object"), + description: "Sorting options like sort[created]=desc or sort[trending]=1" + ), + new OA\Parameter( + name: "videos_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of a specific video to retrieve" + ), + new OA\Parameter( + name: "clean_title", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Clean title of a specific video" + ), + new OA\Parameter( + name: "rowCount", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Maximum number of rows to return" + ), + new OA\Parameter( + name: "current", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Current page number (needed for trending sort)" + ), + new OA\Parameter( + name: "searchPhrase", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Search phrase to filter videos" + ), + new OA\Parameter( + name: "tags_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of a tag to filter videos by" + ), + new OA\Parameter( + name: "catName", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Clean name of the category to include" + ), + new OA\Parameter( + name: "doNotShowCats", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Clean name(s) of categories to exclude" + ), + new OA\Parameter( + name: "channelName", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Channel name to filter videos by" + ), + new OA\Parameter( + name: "playlist", + in: "query", + required: false, + schema: new OA\Schema(type: "boolean"), + description: "If true, returns data in playlist-compatible format" + ), + new OA\Parameter( + name: "videoType", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Type of video (e.g., video, audio, serie, embed)" + ), + new OA\Parameter( + name: "is_serie", + in: "query", + required: false, + schema: new OA\Schema(type: "boolean"), + description: "0 for videos, 1 for series, null for both" + ), + new OA\Parameter( + name: "noRelated", + in: "query", + required: false, + schema: new OA\Schema(type: "boolean"), + description: "If true, disables fetching related videos" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response containing filtered video data" + ) + ] + )] + + public function get_api_video($parameters) { $start = microtime(true); @@ -1105,28 +1784,75 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Updates video metadata such as title, description, category, privacy options, and more. + * Requires proper authentication (APISecret or valid user credentials with permission to edit the video). * - * 'videos_id' the video id what you will update - * ['user' username of the user] - * ['pass' password of the user] - * ['APISecret' to update the video ] * - * ['next_videos_id' id for the next suggested video] - * ['title' String] - * ['status' String] - * ['description' String] - * ['categories_id' int] - * ['can_download' 0 or 1] - * ['can_share'] - * ['only_for_paid' 0 or 1] - * ['video_password' a string with a video password] - * ['trailer1' a trailer URL] - * ['rrating' the valid values are 'g', 'pg', 'pg-13', 'r', 'nc-17', 'ma'] - * ['created' to change the created your user/pass must be a valid admin or you need to provide the APISecret] + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to update. + * @type string|null $user Optional. Username for authentication. + * @type string|null $pass Optional. Password for authentication. + * @type string|null $APISecret Optional. Valid API secret key for authentication. + * @type int|null $next_videos_id Optional. ID of the next suggested video. + * @type string|null $title Optional. New video title. + * @type string|null $status Optional. Video status (e.g., 'public', 'private'). + * @type string|null $description Optional. Video description. + * @type int|null $categories_id Optional. ID of the new category. + * @type int|null $can_download Optional. 1 to allow download, 0 to disallow. + * @type int|null $can_share Optional. 1 to allow sharing, 0 to disallow. + * @type int|null $only_for_paid Optional. 1 for paid-only access, 0 otherwise. + * @type string|null $video_password Optional. Password required to watch the video. + * @type string|null $trailer1 Optional. URL for the trailer. + * @type string|null $rrating Optional. Rating ('g', 'pg', 'pg-13', 'r', 'nc-17', 'ma'). + * @type string|null $created Optional. Custom creation date (requires admin or valid APISecret). + * } * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b - * @return \ApiObject + * + * @return \ApiObject Object containing the result of the update operation or an error message. */ + #[OA\Post( + path: "/api/video_save", + summary: "Update video metadata", + description: << []] ], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video to update"), + new OA\Parameter(name: "user", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Username for authentication"), + new OA\Parameter(name: "pass", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Password for authentication"), + new OA\Parameter(name: "next_videos_id", in: "query", required: false, schema: new OA\Schema(type: "integer", nullable: true), description: "ID of the next suggested video"), + new OA\Parameter(name: "title", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "New video title"), + new OA\Parameter(name: "status", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Video status (e.g., 'public', 'private')"), + new OA\Parameter(name: "description", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Video description"), + new OA\Parameter(name: "categories_id", in: "query", required: false, schema: new OA\Schema(type: "integer", nullable: true), description: "ID of the new category"), + new OA\Parameter(name: "can_download", in: "query", required: false, schema: new OA\Schema(type: "integer", nullable: true), description: "1 to allow download, 0 to disallow"), + new OA\Parameter(name: "can_share", in: "query", required: false, schema: new OA\Schema(type: "integer", nullable: true), description: "1 to allow sharing, 0 to disallow"), + new OA\Parameter(name: "only_for_paid", in: "query", required: false, schema: new OA\Schema(type: "integer", nullable: true), description: "1 for paid-only access, 0 otherwise"), + new OA\Parameter(name: "video_password", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Password required to watch the video"), + new OA\Parameter(name: "trailer1", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "URL of the trailer"), + new OA\Parameter(name: "rrating", in: "query", required: false, schema: new OA\Schema(type: "string", nullable: true), description: "Rating ('g', 'pg', 'pg-13', 'r', 'nc-17', 'ma')"), + new OA\Parameter(name: "created", in: "query", required: false, schema: new OA\Schema(type: "string", format: "date-time", nullable: true), description: "Custom creation date (admin only)") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful operation" + ) + ] + )] + + public function set_api_video_save($parameters) { global $advancedCustomUser; @@ -1207,15 +1933,65 @@ class API extends PluginAbstract } /** - * @param array $parameters - * ['APISecret' to list all videos] - * ['searchPhrase' to search on the categories] - * ['tags_id' the ID of the tag you want to filter] - * ['catName' the clean_APIName of the category you want to filter] - * ['channelName' the channelName of the videos you want to filter] + * Retrieves the total count of videos, with optional filters based on categories, tags, and channel. + * If APISecret is valid, it lists all videos regardless of restrictions. + * + * @param array $parameters { + * @type string $APISecret Optional. If provided and valid, returns all videos. + * @type string $searchPhrase Optional. Phrase to search within the categories. + * @type int $tags_id Optional. ID of the tag to filter the videos. + * @type string $catName Optional. Clean API name of the category to filter videos by. + * @type string $channelName Optional. Name of the channel to filter videos by. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing the total count of videos. */ + #[OA\Get( + path: "/api/videosCount", + summary: "Get total count of videos", + description: "Retrieves the total number of available videos. Filters like tags, category or channel can be applied. If a valid APISecret is provided, restricted videos will also be counted.", + tags: ["Videos"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "searchPhrase", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Search phrase for filtering by title or description" + ), + new OA\Parameter( + name: "tags_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Tag ID to filter videos" + ), + new OA\Parameter( + name: "catName", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Clean category name to filter videos" + ), + new OA\Parameter( + name: "channelName", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Name of the channel to filter videos" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response with totalRows" + ) + ] + )] public function get_api_videosCount($parameters) { global $global; @@ -1233,15 +2009,57 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'videos_id' the video id that will be deleted - * ['APISecret' if passed will not require user and pass] - * ['user' username of the user that will login] - * ['pass' password of the user that will login] + * Deletes a video by its ID if the authenticated user has permission or a valid APISecret is provided. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to delete. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username for authentication (if APISecret is not used). + * @type string $pass Optional. Password for authentication (if APISecret is not used). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object indicating success or failure of the delete operation. */ - public function get_api_video_delete($parameters) + #[OA\Delete( + path: "/api/video_delete", + summary: "Delete a video", + description: "Deletes a video by its ID. Requires the user to be authenticated or provide a valid APISecret. The user must have permission to manage the video.", + tags: ["Videos"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video to delete" + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Username for authentication if APISecret is not used" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password for authentication if APISecret is not used" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful delete operation or an error message" + ) + ] + )] + public function set_api_video_delete($parameters) { global $global; require_once $global['systemRootPath'] . 'objects/video.php'; @@ -1265,16 +2083,80 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'comment' String with the comment - * 'videos_id' the video that will receive the comment - * ['id' the comment id if you will edit some] - * ['APISecret' if passed will not require user and pass] - * ['user' username of the user that will login] - * ['pass' password of the user that will login] + * Creates or updates a comment on a specific video. Requires user authentication or a valid APISecret. + * + * @param array $parameters { + * @type string $comment Required. The content of the comment. + * @type int $videos_id Required. The ID of the video that will receive the comment. + * @type int $id Optional. The ID of the comment to edit. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username of the user posting the comment. + * @type string $pass Optional. Password of the user posting the comment. + * @type int $comments_id Optional. The parent comment ID (for replies). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing the result and the new or updated comment ID. */ + #[OA\Post( + path: "/api/comment", + summary: "Create or edit a comment on a video", + description: "Creates a new comment or edits an existing one. Requires user authentication or a valid APISecret.", + tags: ["Videos", "Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "comment", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Content of the comment" + ), + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video to comment on" + ), + new OA\Parameter( + name: "id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of the comment to edit (optional)" + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Username of the commenter" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password of the commenter" + ), + new OA\Parameter( + name: "comments_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Parent comment ID (for replies)" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful operation" + ) + ] + )] public function set_api_comment($parameters) { global $global; @@ -1307,17 +2189,73 @@ class API extends PluginAbstract } /** - * @param array $parameters - * 'comment' String with the comment - * 'videos_id' the video that will retreive the comments - * ['APISecret' if passed will not require user and pass] - * ['user' username of the user that will login] - * ['pass' password of the user that will login] - * ['rowCount' max numbers of rows] - * ['current' current page] + * Retrieves all comments for a given video, with optional authentication and pagination. + * + * @param array $parameters { + * @type int $videos_id Required. The ID of the video to retrieve comments for. + * @type string $APISecret Optional. If provided and valid, bypasses user authentication. + * @type string $user Optional. Username for authentication. + * @type string $pass Optional. Password for authentication. + * @type int $rowCount Optional. Maximum number of comments to return. + * @type int $current Optional. Current page number for pagination. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing the list of comments with user metadata. */ + #[OA\Get( + path: "/api/comment", + summary: "Retrieve comments for a video", + description: "Fetches all comments associated with a video. Requires video ID and authentication (user/pass or APISecret).", + tags: ["Videos", "Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the video to retrieve comments for" + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Username for authentication" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password for authentication" + ), + new OA\Parameter( + name: "rowCount", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Maximum number of comments to return (pagination)" + ), + new OA\Parameter( + name: "current", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Current page for pagination" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response with comment list" + ) + ] + )] + public function get_api_comment($parameters) { global $global; @@ -1345,13 +2283,55 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Retrieves live schedule information. If a specific ID is provided, returns only that record. + * Requires user authentication. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. The ID of the live schedule to retrieve. + * @type string $user Required. Username for authentication. + * @type string $pass Required. Password for authentication. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * ['live_schedule_id' if you pass it will return a specific live_schedule record] - * 'user' username of the user that will login - * 'pass' password of the user that will login - * @return \ApiObject + * + * @return \ApiObject Object containing the live schedule record(s). */ + #[OA\Get( + path: "/api/live_schedule", + summary: "Get Live schedule", + description: "Retrieves live schedule information for the authenticated user. If a specific ID is provided, returns only that record.", + tags: ["Live"], + parameters: [ + new OA\Parameter( + name: "live_schedule_id", + description: "Optional. The ID of the live schedule to retrieve", + in: "query", + required: false, + schema: new OA\Schema(type: "integer") + ), + new OA\Parameter( + name: "user", + description: "Required. Username for authentication", + in: "query", + required: true, + schema: new OA\Schema(type: "string") + ), + new OA\Parameter( + name: "pass", + description: "Required. Password for authentication", + in: "query", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response with live schedule data" + ) + ] + )] public function get_api_live_schedule($parameters) { if (!User::canStream()) { @@ -1374,13 +2354,56 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Retrieves one or more live schedule records. If `live_schedule_id` is provided, returns only the specified record. + * Requires user authentication. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. The ID of the specific live schedule to retrieve. + * @type string $user Required. Username of the user for authentication. + * @type string $pass Required. Password of the user for authentication. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * ['live_schedule_id' if you pass it will return a specific live_schedule record] - * 'user' username of the user that will login - * 'pass' password of the user that will login - * @return \ApiObject + * + * @return \ApiObject Object containing one or more live schedule entries. */ + #[OA\Delete( + path: "/api/live_schedule_delete", + summary: "Delete a Live schedule", + description: "Deletes a live schedule by ID. Requires authentication.", + tags: ["Live"], + parameters: [ + new OA\Parameter( + name: "live_schedule_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "ID of the live schedule to delete" + ), + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Username for authentication" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Password for authentication" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful deletion" + ) + ] + )] + public function set_api_live_schedule_delete($parameters) { if (!User::canStream()) { @@ -1402,22 +2425,128 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Creates or updates a live schedule record. Allows uploading poster images and setting metadata. + * Requires user authentication with streaming permissions. + * + * @param array $parameters { + * @type int|null $live_schedule_id Optional. If provided, updates the existing record. + * @type int|null $live_servers_id Optional. Live server ID (default is 0). + * @type string|null $base64PNGImageRegular Optional. Base64-encoded regular poster image. + * @type string|null $base64PNGImagePreRoll Optional. Base64-encoded pre-roll poster image. + * @type string|null $base64PNGImagePostRoll Optional. Base64-encoded post-roll poster image. + * @type string $title Required when creating. Title of the live schedule. + * @type string|null $description Optional. Description of the live stream. + * @type string $scheduled_time Required when creating. Date and time in 'YYYY-mm-dd HH:ii:ss' format. + * @type string $status Required. 'a' for active or 'i' for inactive. + * @type string|null $scheduled_password Optional. Password to restrict access to the live stream. + * @type string $user Required. Username of the user. + * @type string $pass Required. Password of the user. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * ['live_servers_id' by default it is 0] - * ['live_schedule_id' if you pass it want to edit a specific record] - * ['base64PNGImageRegular' a png image base64 encoded] - * ['base64PNGImagePreRoll' a png image base64 encoded] - * ['base64PNGImagePostRoll' a png image base64 encoded] - * 'title' - * 'description' - * 'scheduled_time' pass it in the YYYY-mm-dd HH:ii:ss format - * 'status' a for active or i for inactive - * 'scheduled_password' - * 'user' username of the user that will login - * 'pass' password of the user that will login - * @return \ApiObject + * + * @return \ApiObject Object containing the created or updated live_schedule_id. */ + #[OA\Post( + path: "/api/live_schedule", + summary: "Create or update Live schedule", + description: "Creates or updates a live schedule record. Requires authentication and streaming permissions.", + tags: ["Live"], + parameters: [ + new OA\Parameter( + name: "live_schedule_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of the schedule to update (if editing)" + ), + new OA\Parameter( + name: "live_servers_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "ID of the live server" + ), + new OA\Parameter( + name: "base64PNGImageRegular", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Base64 PNG image for the regular thumbnail" + ), + new OA\Parameter( + name: "base64PNGImagePreRoll", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Base64 PNG image for the pre-roll" + ), + new OA\Parameter( + name: "base64PNGImagePostRoll", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Base64 PNG image for the post-roll" + ), + new OA\Parameter( + name: "title", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Title of the live schedule" + ), + new OA\Parameter( + name: "description", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Description of the live schedule" + ), + new OA\Parameter( + name: "scheduled_time", + in: "query", + required: true, + schema: new OA\Schema(type: "string", format: "date-time"), + description: "Scheduled date and time for the live event" + ), + new OA\Parameter( + name: "status", + in: "query", + required: true, + schema: new OA\Schema(type: "string", enum: ["a", "i"]), + description: "Status: 'a' (active) or 'i' (inactive)" + ), + new OA\Parameter( + name: "scheduled_password", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Password required to access the scheduled live" + ), + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Username for authentication" + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Password for authentication" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Live schedule successfully created or updated" + ) + ] + )] + public function set_api_live_schedule($parameters) { $live_schedule_id = 0; @@ -1490,10 +2619,38 @@ class API extends PluginAbstract } /** - * @param array $parameters + * Retrieves live stream statistics using the Live plugin's `stats.json.php` endpoint. + * This will immediately return the JSON response and terminate execution. + * + * @param array $parameters Not used in this endpoint. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject This method exits early and does not return an ApiObject. The response is handled by `stats.json.php`. */ + #[OA\Get( + path: "/api/livestreams", + summary: "Retrieve livestream statistics via the Live plugin", + description: "Fetches real-time statistics using the Live plugin's stats.json.php endpoint.", + tags: ["Live"], + parameters: [ + new OA\Parameter( + name: "parameters", + in: "query", + required: false, + schema: new OA\Schema(type: "object"), + description: "Optional parameters (currently unused)" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response with livestream statistics" + ) + ] + )] + public function get_api_livestreams($parameters) { global $global; @@ -1502,18 +2659,83 @@ class API extends PluginAbstract } /** - * Return a user livestream information - * @param array $parameters - * ['title' Livestream title] - * ['public' 1 = live is listed; 0 = not listed] - * ['APISecret' if passed will not require user and pass] - * ['users_id' the user ID] - * ['resetKey' send resetKey=1 to reset the key] - * ['user' username if does not have the APISecret] - * ['pass' password if does not have the APISecret] + * Creates or updates a livestream record for a user. Can optionally reset the livestream key. + * Requires authentication using either APISecret or user credentials. + * + * @param array $parameters { + * @type string|null $title Optional. The title of the livestream. + * @type int|null $public Optional. 1 for public listing, 0 for private/unlisted. + * @type string|null $APISecret Optional. If provided and valid, bypasses user authentication. + * @type int|null $users_id Optional. The user ID. Required if APISecret is used. + * @type int|null $resetKey Optional. Set to 1 to reset the livestream key. + * @type string|null $user Optional. Username for authentication (if APISecret is not used). + * @type string|null $pass Optional. Password for authentication (if APISecret is not used). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&users_id=1 - * @return \ApiObject + * + * @return \ApiObject Object containing the updated livestream data or an error message. */ + #[OA\Post( + path: "/api/livestream_save", + summary: "Create or update a livestream", + description: "Creates or updates a livestream record for a user. Optionally resets the stream key. Requires authentication.", + tags: ["Live"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "title", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Optional. The title of the livestream." + ), + new OA\Parameter( + name: "public", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Optional. 1 for public, 0 for private." + ), + new OA\Parameter( + name: "users_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Optional. Required if APISecret is used." + ), + new OA\Parameter( + name: "resetKey", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Optional. Set to 1 to reset the stream key." + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Optional. Username for login authentication." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Optional. Password for login authentication." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns updated livestream data or error message" + ) + ] + )] + + public function set_api_livestream_save($parameters) { global $global; @@ -1542,7 +2764,7 @@ class API extends PluginAbstract $trans->setTitle($parameters['title']); $trans->setPublic($parameters['public']); if ($obj->id = $trans->save()) { - if($parameters['resetKey']){ + if ($parameters['resetKey']) { LiveTransmition::resetTransmitionKey($parameters['users_id']); } $trans = LiveTransmition::getFromDb($obj->id, true); @@ -1556,15 +2778,58 @@ class API extends PluginAbstract } /** - * Return a user livestream information - * @param array $parameters - * ['APISecret' if passed will not require user and pass] - * ['users_id' the user ID] - * ['user' username if does not have the APISecret] - * ['pass' password if does not have the APISecret] + * Returns livestream and wallet information for a specific user. + * Requires authentication using either APISecret or user credentials. + * + * @param array $parameters { + * @type string|null $APISecret Optional. If provided and valid, bypasses user authentication. + * @type int|null $users_id Optional. The ID of the user. If not set, uses the logged-in user. + * @type string|null $user Optional. Username for authentication (if APISecret is not used). + * @type string|null $pass Optional. Password for authentication (if APISecret is not used). + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&users_id=1 - * @return \ApiObject + * + * @return \ApiObject Object containing user info, livestream settings, active/live/scheduled streams, and wallet info. */ + #[OA\Get( + path: "/api/user", + summary: "Get user, livestream, and wallet information", + description: "Returns livestream settings and wallet data for a specific user. Authentication is required via APISecret or user credentials.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "users_id", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Optional. ID of the user. Defaults to the logged-in user if not provided." + ), + new OA\Parameter( + name: "user", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Optional. Username for authentication (if APISecret is not used)." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Optional. Password for authentication (if APISecret is not used)." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Object containing user details, livestream settings, active/live/scheduled streams, and wallet info." + ) + ] + )] + public function get_api_user($parameters) { global $global; @@ -1654,19 +2919,47 @@ class API extends PluginAbstract } /** - * Return a users list - * @param array $parameters - * 'APISecret' is required for this call - * ['rowCount' max numbers of rows] - * ['current' current page] - * ['searchPhrase' to search on the user and name columns] - * ['status' if passed will filter active or inactive users] - * ['isAdmin' if passed will filter only admin] - * ['isCompany' if passed will filter only companies] - * ['canUpload' if passed will filter only users that can upload] + * Returns a filtered list of users. Requires a valid APISecret. + * + * @param array $parameters { + * @type string $APISecret Required. API secret key for authentication. + * @type int|null $rowCount Optional. Maximum number of users to return. + * @type int|null $current Optional. Current page number for pagination. + * @type string|null $searchPhrase Optional. Search term to filter by username or name. + * @type string|null $status Optional. Filter by user status: 'a' (active) or 'i' (inactive). + * @type int|null $isAdmin Optional. Filter to return only admin users. + * @type int|null $isCompany Optional. Filter to return only company users. + * @type int|null $canUpload Optional. Filter to return only users who can upload. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&status=a&rowCount=3&searchPhrase=test - * @return \ApiObject + * + * @return \ApiObject Object containing the list of users matching the filters. */ + #[OA\Get( + path: "/api/users_list", + summary: "Get list of users", + description: "Returns a filtered list of users. Requires valid APISecret for authentication.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter(name: "rowCount", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Maximum number of users to return."), + new OA\Parameter(name: "current", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Current page number."), + new OA\Parameter(name: "searchPhrase", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Search term to filter users."), + new OA\Parameter(name: "status", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Filter by status: 'a' (active) or 'i' (inactive)."), + new OA\Parameter(name: "isAdmin", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. 1 to return only admin users."), + new OA\Parameter(name: "isCompany", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. 1 to return only company users."), + new OA\Parameter(name: "canUpload", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. 1 to return only users with upload permissions.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "List of users matching the filters" + ) + ] + )] + public function get_api_users_list($parameters) { global $global; @@ -1704,20 +2997,51 @@ class API extends PluginAbstract } /** - * @param array $parameters - * ['APISecret' to list all videos] - * ['sort' database sort column] - * ['videos_id' the video id (will return only 1 or 0 video)] - * ['clean_title' the video clean title (will return only 1 or 0 video)] - * ['rowCount' max numbers of rows] - * ['current' current page] - * ['searchPhrase' to search on the categories] - * ['tags_id' the ID of the tag you want to filter] - * ['catName' the clean_Name of the category you want to filter] - * ['channelName' the channelName of the videos you want to filter] + * Returns the total number of videos and their combined views count. + * + * @param array $parameters { + * @type string|null $APISecret Optional. If provided and valid, grants access to all videos. + * @type string|null $sort Optional. Sort by column name (e.g., views_count). + * @type int|null $videos_id Optional. Specific video ID to retrieve. + * @type string|null $clean_title Optional. Clean title to retrieve a specific video. + * @type int|null $rowCount Optional. Maximum number of rows to return. + * @type int|null $current Optional. Current page number for pagination. + * @type string|null $searchPhrase Optional. Search term for filtering. + * @type int|null $tags_id Optional. Filter by tag ID. + * @type string|null $catName Optional. Filter by category clean name. + * @type string|null $channelName Optional. Filter by channel name. + * } + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject Object containing totalRows and viewsCount for the filtered video set. */ + #[OA\Get( + path: "/api/videosViewsCount", + summary: "Get videos view count", + description: "Returns the total number of videos and their combined views count. Supports filtering by ID, title, tags, categories, and channels.", + tags: ["Videos"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter(name: "sort", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Sort by column name (e.g., views_count)."), + new OA\Parameter(name: "videos_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Specific video ID."), + new OA\Parameter(name: "clean_title", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Clean title of the video."), + new OA\Parameter(name: "rowCount", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Max number of rows to return."), + new OA\Parameter(name: "current", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Page number for pagination."), + new OA\Parameter(name: "searchPhrase", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Search filter."), + new OA\Parameter(name: "tags_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Filter by tag ID."), + new OA\Parameter(name: "catName", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Filter by category clean name."), + new OA\Parameter(name: "channelName", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional. Filter by channel name.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Object containing totalRows and viewsCount" + ) + ] + )] + public function get_api_videosViewsCount($parameters) { global $global; @@ -1750,11 +3074,57 @@ class API extends PluginAbstract } /** - * @param array $parameters - * Return all channels on this site + * Returns a list of all channels on the site. + * + * @param array $parameters Currently unused. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject List of channels with basic information: + * - id: Channel ID + * - photo: Channel photo URL + * - channelLink: Channel link URL + * - name: User Display name + * - channelName: Unique channel identifier */ + #[OA\Get( + path: "/api/channels", + summary: "Get all channels", + description: "Returns a list of all channels on the platform. Each channel includes ID, photo URL, name, and link.", + tags: ["Users"], + responses: [ + new OA\Response( + response: 200, + description: "Successful response with channel list", + content: new OA\JsonContent( + type: "object", + properties: [ + new OA\Property(property: "error", type: "boolean", example: false), + new OA\Property(property: "message", type: "string", example: ""), + new OA\Property( + property: "response", + type: "array", + items: new OA\Items( + type: "object", + properties: [ + new OA\Property(property: "id", type: "integer", example: 12), + new OA\Property(property: "photo", type: "string", format: "uri", example: "https://example.com/photos/12.jpg"), + new OA\Property(property: "channelLink", type: "string", format: "uri", example: "https://example.com/channel/username"), + new OA\Property(property: "name", type: "string", example: "John Doe"), + new OA\Property(property: "channelName", type: "string", example: "johndoe") + ] + ) + ), + new OA\Property(property: "users_id", type: "integer", example: 1), + new OA\Property(property: "user_age", type: "integer", example: 35), + new OA\Property(property: "session_id", type: "string", example: "abcd1234efgh5678") + ] + ) + ) + ] + )] + + public function get_api_channels($parameters) { global $global; @@ -1775,14 +3145,38 @@ class API extends PluginAbstract } /** + * Returns a single Program (Playlist) from the site. + * * @param array $parameters - * Return a single Program (Playlists) on this site - * 'playlists_id' or 'videos_id' if it is a serie - * If you pass the playlists_id it will only return if the program belongs to you - * if you pass videos_id it will return only if you have rights to watch the video + * - playlists_id (int) Optional. The playlist ID. Required if videos_id is not provided. + * - videos_id (int) Optional. If provided, retrieves the program associated with the video (if it's part of a series). + * + * Behavior: + * - If `playlists_id` is provided, the program is returned only if it belongs to the logged-in user. + * - If `videos_id` is provided, the function will resolve the associated playlist and ensure the user has permission to watch it. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=12 - * @return \ApiObject + * + * @return \ApiObject Object with the list of videos in the program (property: videos). */ + #[OA\Get( + path: "/api/program", + summary: "Get a specific program (playlist)", + description: "Returns a single playlist associated with a user or a video. Requires either playlists_id or videos_id.", + tags: ["Programs"], + parameters: [ + new OA\Parameter(name: "playlists_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Playlist ID. Required if videos_id is not provided."), + new OA\Parameter(name: "videos_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. Video ID to resolve the associated playlist.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Playlist and its videos" + ) + ] + )] + public function get_api_program($parameters) { global $global; @@ -1815,13 +3209,38 @@ class API extends PluginAbstract } /** + * Returns all Programs (Playlists) available for the authenticated user. + * * @param array $parameters - * Return all Programs (Playlists) on this site - * ['onlyWithVideos' can be 0 or 1 return only programs that contain videos, the default is 1] - * ['returnFavoriteAndWatchLater' can be 0 or 1, the default is 0] + * - onlyWithVideos (int) Optional. 0 or 1. If 1, only returns programs that contain videos. Default is 1. + * - returnFavoriteAndWatchLater (int) Optional. 0 or 1. If 1, includes "favorite" and "watch later" programs. Default is 0. + * + * Notes: + * - The user must be logged in. + * - Each program includes metadata such as ID, photo, username, link, and associated videos. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject List of programs with metadata. */ + #[OA\Get( + path: "/api/programs", + summary: "Get user programs (playlists)", + description: "Returns all playlists belonging to the authenticated user. Can include only those with videos and optionally favorites/watch later.", + tags: ["Programs"], + parameters: [ + new OA\Parameter(name: "onlyWithVideos", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. 1 to return only playlists with videos."), + new OA\Parameter(name: "returnFavoriteAndWatchLater", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional. 1 to include favorite and watch later playlists.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "List of playlists with metadata and videos" + ) + ] + )] + public function get_api_programs($parameters) { global $global; @@ -1859,13 +3278,54 @@ class API extends PluginAbstract } /** + * Create a new Program (Playlist). + * * @param array $parameters - * Create new programs - * 'name' the new program name - * 'status' the new program status ['public', 'private', 'unlisted', 'favorite', 'watch_later'] + * - name (string, required) The name of the new program. + * - status (string, optional) The program visibility status. Accepted values: 'public', 'private', 'unlisted', 'favorite', 'watch_later'. + * + * Notes: + * - The user must be logged in. + * - The PlayLists plugin must be enabled. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&name=NewPL&status=unlisted - * @return \ApiObject + * + * @return \ApiObject Success or error status. */ + #[OA\Post( + path: "/api/create_programs", + summary: "Create a new program (playlist)", + description: "Creates a new playlist for the logged-in user. The PlayLists plugin must be enabled.", + tags: ["Programs"], + parameters: [ + new OA\Parameter( + name: "name", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The name of the new program." + ), + new OA\Parameter( + name: "status", + in: "query", + required: false, + schema: new OA\Schema( + type: "string", + enum: ["public", "private", "unlisted", "favorite", "watch_later"] + ), + description: "Optional. Visibility status. Values: public, private, unlisted, favorite, watch_later." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Success or error information", + ) + ] + )] + + public function set_api_create_programs($parameters) { global $global; @@ -1897,12 +3357,41 @@ class API extends PluginAbstract } /** + * Delete an existing Program (Playlist). + * * @param array $parameters - * Delete programs - * 'playlists_id' the id of the program you want to delete + * - playlists_id (int, required) The ID of the program to delete. + * + * Notes: + * - The user must be logged in and must own the playlist. + * - The PlayLists plugin must be enabled. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&playlists_id=12 - * @return \ApiObject + * + * @return \ApiObject Success or error status. */ + #[OA\Delete( + path: "/api/delete_programs", + summary: "Delete an existing program (playlist)", + description: "Deletes a playlist owned by the logged-in user. The PlayLists plugin must be enabled.", + tags: ["Programs"], + parameters: [ + new OA\Parameter( + name: "playlists_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the program to delete." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Success or error information", + ) + ] + )] public function set_api_delete_programs($parameters) { global $global; @@ -1935,14 +3424,61 @@ class API extends PluginAbstract } /** + * Add or remove a video from a Program (Playlist). + * * @param array $parameters - * Return all Programs (Playlists) on this site - * 'videos_id' - * 'playlists_id' , - * 'add' 1 = will add, 0 = will remove, + * - videos_id (int, required) The ID of the video. + * - playlists_id (int, required) The ID of the playlist (program). + * - add (int, required) Use 1 to add the video to the playlist or 0 to remove it. + * + * Notes: + * - The user must be logged in. + * - The PlayLists plugin must be enabled. + * - The user must be the owner of the playlist. + * - The user must have permission to add the video to the playlist. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=11&playlists_id=10&add=1 - * @return \ApiObject + * + * @return \ApiObject Success or error status. */ + #[OA\Post( + path: "/api/programs", + summary: "Add or remove video from a program (playlist)", + description: "Adds or removes a video to/from a playlist. The user must own the playlist and have permission.", + tags: ["Programs"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video." + ), + new OA\Parameter( + name: "playlists_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the playlist." + ), + new OA\Parameter( + name: "add", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "Set 1 to add, 0 to remove." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Success or error information", + ) + ] + )] + + public function set_api_programs($parameters) { global $global; @@ -1983,12 +3519,44 @@ class API extends PluginAbstract } /** + * Retrieve all subscribers for a specific user. + * * @param array $parameters - * Return all Subscribers from an user - * 'users_id' users ID + * - users_id (int, required) The user ID to retrieve the subscribers for. + * - APISecret (string, required) A valid API secret to authenticate the request. + * + * Notes: + * - Caches the result for 1 hour (3600 seconds). + * - Requires a valid APISecret. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?users_id=1&APIName={APIName}&APISecret={APISecret} - * @return \ApiObject + * + * @return \ApiObject A list of subscribers or an error object. */ + #[OA\Get( + path: "/api/subscribers", + summary: "Retrieve subscribers for a user", + description: "Returns all subscribers for the specified user. Requires a valid APISecret. Cached for 1 hour.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "users_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The user ID to retrieve the subscribers for." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "List of subscribers or error", + ) + ] + )] + public function get_api_subscribers($parameters) { global $global; @@ -2011,11 +3579,50 @@ class API extends PluginAbstract } /** + * Retrieve all categories available on the site. + * * @param array $parameters - * Return all categories on this site + * (No parameters are required for this request) + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return \ApiObject + * + * @return \ApiObject A list of all site categories with basic information such as ID, icon class, name, and totals. */ + #[OA\Get( + path: "/api/categories", + summary: "Retrieve all categories", + description: "Returns a list of all site categories including ID, icon class, name, and total counts.", + tags: ["Videos"], + responses: [ + new OA\Response( + response: 200, + description: "Successful response with a list of categories", + content: new OA\JsonContent( + type: "object", + properties: [ + new OA\Property(property: "error", type: "boolean", example: false), + new OA\Property(property: "message", type: "string", example: ""), + new OA\Property(property: "response", type: "array", items: new OA\Items( + type: "object", + properties: [ + new OA\Property(property: "id", type: "integer", example: 3), + new OA\Property(property: "iconClass", type: "string", example: "fa fa-film"), + new OA\Property(property: "hierarchyAndName", type: "string", example: "Movies"), + new OA\Property(property: "name", type: "string", example: "Movies"), + new OA\Property(property: "clean_name", type: "string", example: "movies"), + new OA\Property(property: "fullTotal", type: "integer", example: 24), + new OA\Property(property: "total", type: "integer", example: 10) + ] + )), + new OA\Property(property: "users_id", type: "integer", example: 1), + new OA\Property(property: "user_age", type: "integer", example: 35), + new OA\Property(property: "session_id", type: "string", example: "abcd1234") + ] + ) + ) + ] + )] + public function get_api_categories($parameters) { global $global; @@ -2038,11 +3645,38 @@ class API extends PluginAbstract } /** + * Retrieve the number of likes for a specific video. + * * @param array $parameters - * 'videos_id' the video ID what you want to get the likes + * @param int $parameters['videos_id'] (required) The ID of the video for which you want to retrieve the like count. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1 - * @return \ApiObject + * + * @return \ApiObject An object containing the total number of likes for the specified video. */ + #[OA\Get( + path: "/api/likes", + summary: "Get like count for a video", + description: "Returns the number of likes for the specified video ID.", + tags: ["Likes"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video to retrieve like count for." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Like count for the video", + ) + ] + )] + public function get_api_likes($parameters) { global $global; @@ -2054,53 +3688,199 @@ class API extends PluginAbstract } /** - * @param array $parameters (all parameters are mandatories) - * 'videos_id' the video ID what you want to send the like - * 'user' username of the user that will login - * 'pass' password of the user that will login + * Register a "like" for a specific video from a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to like. + * @param string $parameters['user'] The username of the user performing the like action. + * @param string $parameters['pass'] The password of the user. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 - * @return \ApiObject + * + * @return \ApiObject Result of the like operation. */ + #[OA\Post( + path: "/api/like", + summary: "Register a like for a video", + description: "Registers a like on a specific video from an authenticated user.", + tags: ["Likes"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video." + ), + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The username of the user." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The password of the user." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Result of the like operation", + ) + ] + )] + + public function set_api_like($parameters) { return $this->like($parameters, 1); } /** - * @param array $parameters (all parameters are mandatories) - * 'videos_id' the video ID what you want to send the like - * 'user' username of the user that will login - * 'pass' password of the user that will login + * Register a "dislike" for a specific video from a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to dislike. + * @param string $parameters['user'] The username of the user performing the dislike action. + * @param string $parameters['pass'] The password of the user. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 - * @return \ApiObject + * + * @return \ApiObject Result of the dislike operation. */ + #[OA\Post( + path: "/api/dislike", + summary: "Register a dislike for a video", + description: "Registers a dislike on a specific video from an authenticated user.", + tags: ["Likes"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video." + ), + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The username of the user." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The password of the user." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Result of the dislike operation", + ) + ] + )] + + public function set_api_dislike($parameters) { return $this->like($parameters, -1); } /** - * @param array $parameters (all parameters are mandatories) - * 'videos_id' the video ID what you want to send the like - * 'user' username of the user that will login - * 'pass' password of the user that will login + * Remove a previously registered like/dislike from a specific video by a logged-in user. + * + * @param array $parameters (all parameters are mandatory) + * @param int $parameters['videos_id'] The ID of the video to remove the like/dislike. + * @param string $parameters['user'] The username of the user performing the action. + * @param string $parameters['pass'] The password of the user. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=1&user=admin&pass=123 - * @return \ApiObject + * + * @return \ApiObject Result of the like removal operation. */ + #[OA\Post( + path: "/api/removelike", + summary: "Remove like or dislike from a video", + description: "Removes a previously registered like or dislike from a specific video by an authenticated user.", + tags: ["Likes"], + parameters: [ + new OA\Parameter( + name: "videos_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer"), + description: "The ID of the video." + ), + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Username of the user." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Password of the user." + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Result of like removal operation", + ) + ] + )] public function set_api_removelike($parameters) { return $this->like($parameters, 0); } /** + * Authenticate a user and return a session token or error. * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * ['encodedPass' tell the script id the password submitted is raw or encrypted] + * @param string $parameters['user'] The username of the user. + * @param string $parameters['pass'] The user's password (either raw or encrypted). + * @param bool [$parameters['encodedPass']] Optional. Set to true if the password is already encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON encoded authentication response or error message. */ + #[OA\Get( + path: "/api/signIn", + summary: "Authenticate user and return session", + description: "Authenticates a user using username and password (raw or encoded). Returns session token or error.", + tags: ["Users"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if password is already encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + description: "JSON with user authentication data or error", + content: new OA\JsonContent(type: "string", example: "{\"error\":false,\"message\":\"Logged in\",\"session_id\":\"abcd1234\"}") + ) + ] + )] + public function get_api_signIn($parameters) { global $global; @@ -2110,20 +3890,97 @@ class API extends PluginAbstract } /** + * Register a new user on the platform. * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * 'email' email of the user - * 'name' real name of the user - * ['emailVerified' 1 = email verified] - * ['canCreateMeet' 1 = Can create meetings] - * ['canStream' 1 = Can make live streams] - * ['canUpload' 1 = Can upload files] - * 'APISecret' mandatory for security reasons + * @param string $parameters['user'] The desired username of the user. + * @param string $parameters['pass'] The password for the user. + * @param string $parameters['email'] The email address of the user. + * @param string $parameters['name'] The full name of the user. + * @param int [$parameters['emailVerified']] Optional. Set to 1 if the user's email is already verified. + * @param int [$parameters['canCreateMeet']] Optional. Set to 1 if the user is allowed to create meetings. + * @param int [$parameters['canStream']] Optional. Set to 1 if the user is allowed to start live streams. + * @param int [$parameters['canUpload']] Optional. Set to 1 if the user is allowed to upload videos. + * @param string $parameters['APISecret'] Required. Secret key to authorize the API request. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&user=admin&pass=123&email=me@mysite.com&name=Yeshua - * @return string + * + * @return string JSON encoded success or error response. */ + #[OA\Post( + path: "/api/signUp", + summary: "Register a new user", + description: "Registers a new user in the platform. Requires APISecret to authorize the request.", + tags: ["Users"], + parameters: [ + new OA\Parameter( + name: "user", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Desired username." + ), + new OA\Parameter( + name: "pass", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Password for the user." + ), + new OA\Parameter( + name: "email", + in: "query", + required: true, + schema: new OA\Schema(type: "string", format: "email"), + description: "User's email address." + ), + new OA\Parameter( + name: "name", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "Full name of the user." + ), + new OA\Parameter( + name: "emailVerified", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Set to 1 if the email is already verified." + ), + new OA\Parameter( + name: "canCreateMeet", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Set to 1 to allow user to create meetings." + ), + new OA\Parameter( + name: "canStream", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Set to 1 to allow user to start live streams." + ), + new OA\Parameter( + name: "canUpload", + in: "query", + required: false, + schema: new OA\Schema(type: "integer"), + description: "Set to 1 to allow user to upload videos." + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "JSON string with success or error message", + content: new OA\JsonContent( + type: "string", + example: "{\"error\":false,\"message\":\"User registered successfully\"}" + ) + ) + ] + )] public function set_api_signUp($parameters) { global $global; @@ -2149,6 +4006,19 @@ class API extends PluginAbstract exit; } + /** + * Handle like/dislike/removal actions for a video. + * + * @param array $parameters + * @param int $like The like value to apply: + * 1 = like, + * -1 = dislike, + * 0 = remove like/dislike. + * + * @param int $parameters['videos_id'] The ID of the video to apply the like action. + * + * @return \ApiObject The updated like status object or an error message. + */ private function like($parameters, $like) { global $global; @@ -2170,17 +4040,47 @@ class API extends PluginAbstract } /** - * If you do not pass the user and password, it will always show ads, if you pass it the script will check if will display ads or not + * Returns a VMAP (Video Multiple Ad Playlist) XML response for ads playback. + * + * If no user credentials are provided, ads will always be shown. + * If valid user credentials are passed, the script will decide based on user permissions whether to show ads. + * * @param array $parameters - * 'videos_id' the video id to calculate the ads length - * ['optionalAdTagUrl' a tag number 1 or 2 or 3 or 4 to use another tag, if do not pass it will use the default tag] - * ['user' username of the user] - * ['pass' password of the user] - * ['encodedPass' tell the script id the password submitted is raw or encrypted] - * @example for XML response: {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true&optionalAdTagUrl=2 - * @example for JSON response: {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true&optionalAdTagUrl=2&json=1 - * @return string + * @param int $parameters['videos_id'] Required. The video ID used to calculate the ads. + * @param int [$parameters['optionalAdTagUrl']] Optional. A tag identifier (1, 2, 3, or 4) to select a different ad tag. Defaults to the default tag if not passed. + * @param string [$parameters['user']] Optional. Username to check permissions for ad display. + * @param string [$parameters['pass']] Optional. Password of the user. + * @param bool [$parameters['encodedPass']] Optional. Indicates if the password is encrypted. + * + * @example XML Response: + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=secret&encodedPass=true&optionalAdTagUrl=2 + * @example JSON Response: + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=secret&encodedPass=true&optionalAdTagUrl=2&json=1 + * + * @return string XML or JSON response depending on the request. */ + #[OA\Get( + path: "/api/vmap", + summary: "Get VMAP ad playlist", + description: "Returns a VMAP XML or JSON response based on ad tag and user permission. Supports plugins like GoogleAds_IMA, AD_Server, AdsForJesus.", + tags: ["Ads"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video for ad computation."), + new OA\Parameter(name: "optionalAdTagUrl", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Optional tag ID (1 to 4) to choose a different ad tag."), + new OA\Parameter(name: "user", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional username."), + new OA\Parameter(name: "pass", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional password."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted."), + new OA\Parameter(name: "json", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true to receive JSON instead of XML.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "VMAP XML or JSON content" + ) + ] + )] + public function get_api_vmap($parameters) { global $global; @@ -2204,16 +4104,43 @@ class API extends PluginAbstract } /** - * If you do not pass the user and password, it will always show ads, if you pass it the script will check if will display ads or not + * Returns a VAST (Video Ad Serving Template) XML response for ad playback. + * + * If no user credentials are provided, ads will always be shown. + * If valid user credentials are passed, the system will determine whether ads should be shown to the user. + * * @param array $parameters - * 'videos_id' the video id to calculate the ads length - * ['optionalAdTagUrl' a tag number 1 or 2 or 3 or 4 to use another tag, if do not pass it will use the default tag] - * ['user' username of the user] - * ['pass' password of the user] - * ['encodedPass' tell the script id the password submitted is raw or encrypted] + * @param int $parameters['videos_id'] Required. The video ID used to determine ad eligibility. + * @param int [$parameters['optionalAdTagUrl']] Optional. Tag identifier (1, 2, 3, or 4) to select a different ad tag. Defaults to the default tag if not provided. + * @param string [$parameters['user']] Optional. Username of the user. + * @param string [$parameters['pass']] Optional. Password of the user. + * @param bool [$parameters['encodedPass']] Optional. If true, indicates that the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true&optionalAdTagUrl=2 - * @return string + * + * @return string XML response (VAST format) */ + #[OA\Get( + path: "/api/vast", + summary: "Get VAST ad response", + description: "Returns a VAST XML response for ad playback, using available ad plugins and user access validation.", + tags: ["Ads"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video."), + new OA\Parameter(name: "optionalAdTagUrl", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Tag ID to override default ad tag."), + new OA\Parameter(name: "user", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional username."), + new OA\Parameter(name: "pass", in: "query", required: false, schema: new OA\Schema(type: "string"), description: "Optional password."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "VAST XML response" + ) + ] + )] + public function get_api_vast($parameters) { global $global; @@ -2224,13 +4151,34 @@ class API extends PluginAbstract } /** - * Return the location based on the provided IP + * Return the location based on the provided IP address. + * * @param array $parameters - * 'APISecret' mandatory for security reasons - * 'ip' Ip to verify + * @param string $parameters['APISecret'] Required. Secret key for API access. + * @param string $parameters['ip'] Required. The IP address to retrieve location data for. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&ip=2.20.147.123 - * @return string + * + * @return \ApiObject Location data including country, city, latitude, longitude, etc., or an error message if not available. */ + #[OA\Get( + path: "/api/IP2Location", + summary: "Get location from IP address", + description: "Returns geolocation data based on the provided IP. Requires valid APISecret and the User_Location plugin enabled.", + tags: ["API"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter(name: "ip", in: "query", required: true, schema: new OA\Schema(type: "string", format: "ipv4"), description: "IP address to look up.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Location data or error message", + ) + ] + )] + public function get_api_IP2Location($parameters) { global $global; @@ -2250,14 +4198,36 @@ class API extends PluginAbstract } /** - * Return all favorites from a user + * Return all favorite videos from a specific user. + * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON-encoded list of favorite playlists and videos. */ + #[OA\Get( + path: "/api/favorite", + summary: "Get favorite videos for the logged-in user", + description: "Returns all playlists and videos marked as favorite by the authenticated user. Requires PlayLists plugin enabled.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON list of favorite playlists and videos" + ) + ] + )] + public function get_api_favorite($parameters) { $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); @@ -2285,29 +4255,76 @@ class API extends PluginAbstract } /** - * add a video into a user favorite play list + * Add a video to the user's favorite playlist. + * * @param array $parameters - * 'videos_id' the video id that you want to add - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['videos_id'] Required. ID of the video to add. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON result indicating success or failure. */ + #[OA\Post( + path: "/api/favorite", + summary: "Add video to user's favorite playlist", + description: "Adds a video to the authenticated user's favorite playlist.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video to add."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "If true, the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Success or error message as JSON" + ) + ] + )] + public function set_api_favorite($parameters) { $this->favorite($parameters, true); } /** + * Remove a video from the user's favorite playlist. + * * @param array $parameters - * 'videos_id' the video id that you want to remove - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['videos_id'] Required. ID of the video to remove. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. If true, indicates the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON result indicating success or failure. */ + #[OA\Post( + path: "/api/removeFavorite", + summary: "Remove video from user's favorite playlist", + description: "Removes a video from the authenticated user's favorite playlist.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video to remove."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "If true, the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Success or error message as JSON" + ) + ] + )] + public function set_api_removeFavorite($parameters) { $this->favorite($parameters, false); @@ -2332,14 +4349,36 @@ class API extends PluginAbstract } /** - * Return all watch_later from a user + * Return all videos in the "watch later" playlist for a user. + * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON encoded list of videos marked as watch later. */ + #[OA\Get( + path: "/api/watch_later", + summary: "Get 'watch later' videos", + description: "Returns all videos from the authenticated user's 'watch later' playlist. Requires login and the PlayLists plugin enabled.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded list of watch later videos" + ) + ] + )] + public function get_api_watch_later($parameters) { $plugin = AVideoPlugin::loadPluginIfEnabled("PlayLists"); @@ -2371,29 +4410,76 @@ class API extends PluginAbstract } /** - * add a video into a user watch_later play list + * Add a video to the user's "watch later" playlist. + * * @param array $parameters - * 'videos_id' the video id that you want to add - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['videos_id'] Required. ID of the video to be added. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON encoded result message. */ + #[OA\Post( + path: "/api/watch_later", + summary: "Add video to 'watch later' playlist", + description: "Adds a video to the user's 'watch later' playlist. User must be logged in.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video to be added."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded result message" + ) + ] + )] + public function set_api_watch_later($parameters) { $this->watch_later($parameters, true); } /** + * Remove a video from the user's "watch later" playlist. + * * @param array $parameters - * 'videos_id' the video id that you want to remove - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['videos_id'] Required. ID of the video to be removed. + * @param string $parameters['user'] Required. Username of the user. + * @param string $parameters['pass'] Required. Password of the user. + * @param bool $parameters['encodedPass'] Optional. Indicates if the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&videos_id=3&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * + * @return string JSON encoded result message. */ + #[OA\Post( + path: "/api/removeWatch_later", + summary: "Remove video from 'watch later' playlist", + description: "Removes a video from the user's 'watch later' playlist. User must be logged in.", + tags: ["Video", "Users"], + parameters: [ + new OA\Parameter(name: "videos_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the video to be removed."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded result message" + ) + ] + )] + public function set_api_removeWatch_later($parameters) { $this->watch_later($parameters, false); @@ -2417,18 +4503,41 @@ class API extends PluginAbstract } /** + * Send a message using the Chat2 plugin. + * * @param array $parameters - * Try this API here - * 'message' the message for the chat - * ['users_id'] User's ID to what this message will be sent to (send the users_id or room_users_id) - * ['room_users_id'] User's ID from the channel where this message will be sent to (send the users_id or room_users_id) - * 'message' URL encoded message - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param string $parameters['message'] Required. The message content, must be URL encoded. + * @param int $parameters['users_id'] Optional. The ID of the user to send the message to. + * @param int $parameters['room_users_id'] Optional. The ID of the channel or room to send the message to. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Indicates whether the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&message=HelloWorld&users_id=2&room_users_id=4&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * @return string JSON encoded result message. */ + #[OA\Post( + path: "/api/chat2_message", + summary: "Send a chat message (Chat2 plugin)", + description: "Sends a message to a user or room using the Chat2 plugin. User must be authenticated.", + tags: ["Chat"], + parameters: [ + new OA\Parameter(name: "message", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Message content (URL encoded)."), + new OA\Parameter(name: "users_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "ID of the recipient user."), + new OA\Parameter(name: "room_users_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "ID of the room/channel."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the sender."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the sender."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded result message" + ) + ] + )] + public function set_api_chat2_message($parameters) { global $global; @@ -2447,19 +4556,41 @@ class API extends PluginAbstract } /** + * Retrieve chat messages using the Chat2 plugin. + * * @param array $parameters - * The sample here will return 10 messages - * Try this API here - * ['to_users_id'] User's ID where this message was private sent to - * ['lower_then_id'] Chat message ID to filter the message search. will only return messages before that chat id - * ['greater_then_id'] Chat message ID to filter the message search. will only return messages after that chat id - * 'message' URL encoded message - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['to_users_id'] Optional. User ID for whom the private messages are intended. + * @param int $parameters['lower_then_id'] Optional. Only messages with ID less than this value will be returned. + * @param int $parameters['greater_then_id'] Optional. Only messages with ID greater than this value will be returned. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Indicates whether the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&greater_then_id=88&lower_then_id=98&to_users_id=2&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * @return string JSON encoded chat messages. */ + #[OA\Get( + path: "/api/chat2_chat", + summary: "Get private chat messages (Chat2 plugin)", + description: "Retrieves private chat messages between users. Filters by message ID range if provided.", + tags: ["Chat"], + parameters: [ + new OA\Parameter(name: "to_users_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Recipient user ID."), + new OA\Parameter(name: "lower_then_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Return messages with ID lower than this."), + new OA\Parameter(name: "greater_then_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Return messages with ID greater than this."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username for authentication."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password for authentication."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded chat messages" + ) + ] + )] + public function get_api_chat2_chat($parameters) { global $global; @@ -2488,19 +4619,41 @@ class API extends PluginAbstract } /** + * Retrieve public chat messages from a specific room using the Chat2 plugin. + * * @param array $parameters - * The sample here will return 10 messages id greater then 88 and lower then 98 - * Try this API here - * ['room_users_id'] User's ID (channel) where this message was public sent to - * ['lower_then_id'] Chat message ID to filter the message search. will only return messages before that chat id - * ['greater_then_id'] Chat message ID to filter the message search. will only return messages after that chat id - * 'message' URL encoded message - * 'user' username of the user - * 'pass' password of the user - * 'encodedPass' tell the script id the password submitted is raw or encrypted + * @param int $parameters['room_users_id'] Required. User ID of the room/channel from which to retrieve messages. + * @param int $parameters['lower_then_id'] Optional. Only messages with ID less than this will be returned. + * @param int $parameters['greater_then_id'] Optional. Only messages with ID greater than this will be returned. + * @param string $parameters['user'] Required. Username for authentication. + * @param string $parameters['pass'] Required. Password for authentication. + * @param bool $parameters['encodedPass'] Optional. Whether the password is encrypted. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&greater_then_id=88&lower_then_id=98&room_users_id=2&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * @return string JSON encoded chat messages from the specified room. */ + #[OA\Get( + path: "/api/chat2_room", + summary: "Get room chat messages (Chat2 plugin)", + description: "Retrieves public chat messages from a specific room/channel. Filters by message ID range if provided.", + tags: ["Chat"], + parameters: [ + new OA\Parameter(name: "room_users_id", in: "query", required: true, schema: new OA\Schema(type: "integer"), description: "ID of the room/channel."), + new OA\Parameter(name: "lower_then_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Return messages with ID lower than this."), + new OA\Parameter(name: "greater_then_id", in: "query", required: false, schema: new OA\Schema(type: "integer"), description: "Return messages with ID greater than this."), + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username for authentication."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password for authentication."), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean"), description: "Set to true if the password is encrypted.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON encoded room messages" + ) + ] + )] + public function get_api_chat2_room($parameters) { global $global; @@ -2535,11 +4688,33 @@ class API extends PluginAbstract } /** - * @param array $parameters - * Return available locales translations + * Retrieve available locale translations on the site. + * + * @param array $parameters Not used in this request. + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName} - * @return string + * + * @return \ApiObject + * { + * "default": "en_US", // Default language configured in the system + * "options": ["en_US", "pt_BR"], // List of enabled language options + * "isRTL": false // Boolean indicating if the current language is RTL + * } */ + #[OA\Get( + path: "/api/locales", + summary: "Get available site locales", + description: "Returns the default language, list of enabled locales, and RTL status.", + tags: ["API"], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "JSON with default language, enabled options, and RTL flag" + ) + ] + )] + public function get_api_locales($parameters) { global $global, $config; @@ -2551,12 +4726,38 @@ class API extends PluginAbstract } /** + * Retrieve translations for a specific language. + * * @param array $parameters - * 'language' specify what translation array the API should return, for example cn = chinese - * Return available locales translations + * 'language' (required) ISO code of the language to retrieve translations for (e.g., 'cn' for Chinese). + * * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&language=cn - * @return string + * + * @return \ApiObject + * Returns a PHP array `$t` containing key-value translation pairs for the requested language. + * + * Error handling: + * - Returns error if 'language' parameter is missing. + * - Returns error if the language file does not exist. + * - Returns error if the translation array `$t` is empty. */ + #[OA\Get( + path: "/api/locale", + summary: "Get translation keys for a specific language", + description: "Returns key-value translations for the specified language ISO code. Example: 'cn' for Chinese.", + tags: ["API"], + parameters: [ + new OA\Parameter(name: "language", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Language ISO code (e.g., en, pt, cn).") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Translation key-value pairs" + ) + ] + )] + public function get_api_locale($parameters) { global $global, $config; @@ -2579,14 +4780,46 @@ class API extends PluginAbstract } /** + * Update user profile and/or background images using URLs. + * * @param array $parameters - * ['APISecret' mandatory for security reasons - required] - * ['user' username of the user - required] - * ['backgroundImg' URL path of the image - optional] - * ['profileImg' URL path of the image - optional] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&user=admin + * - 'APISecret' (string, required): Secret key for API authentication. + * - 'user' (string, required): Username of the user to update. + * - 'backgroundImg' (string, optional): URL to the new background image. + * - 'profileImg' (string, optional): URL to the new profile image. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret}&user=admin + * &backgroundImg=https%3A%2F%2Fexample.com%2Fbackground.jpg&profileImg=https%3A%2F%2Fexample.com%2Fprofile.jpg + * * @return \ApiObject + * Returns an object with updated image info or error messages. + * + * Notes: + * - Both image parameters are optional; if omitted, the corresponding image will not be updated. + * - Requires a valid API secret. + * - If user does not exist, returns an error. */ + #[OA\Post( + path: "/api/userImages", + summary: "Update user's profile and background images", + description: "Updates the profile and/or background image for a user, using image URLs. Requires APISecret.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "backgroundImg", in: "query", required: false, schema: new OA\Schema(type: "string", format: "uri"), description: "URL to background image."), + new OA\Parameter(name: "profileImg", in: "query", required: false, schema: new OA\Schema(type: "string", format: "uri"), description: "URL to profile image.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Updated user image information or error message" + ) + ] + )] + public function set_api_userImages($parameters) { global $global; @@ -2607,15 +4840,47 @@ class API extends PluginAbstract } /** + * Returns the list of scheduled meetings for the authenticated user. * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * ['encodedPass' tell the script id the password submitted is raw or encrypted] - * ['time' default is today but the options are [today|upcoming|past]] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true - * @return string + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - 'encodedPass' (bool, optional): Indicates if the password is encrypted (default: false). + * - 'time' (string, optional): Meeting timeframe filter. Accepted values: [today|upcoming|past]. Default: today. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&time=past + * + * @return \ApiObject + * Returns an array of meetings with their details. If user is a moderator or has access, the room password is included. + * Returns a message if there are no meetings scheduled. + * + * Notes: + * - Requires the Meet plugin to be enabled. + * - Requires valid user login credentials. + * - Meeting room password is only included if the user has permission to view it. */ + #[OA\Get( + path: "/api/meet", + summary: "Get list of scheduled meetings for authenticated user", + description: "Returns all scheduled meetings (past, upcoming, or today) for the logged-in user. Includes room password when authorized.", + tags: ["Meet"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean")), + new OA\Parameter(name: "time", in: "query", required: false, schema: new OA\Schema(type: "string", enum: ["today", "upcoming", "past"])) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Array of meeting objects with optional passwords" + ) + ] + )] + public function get_api_meet($parameters) { global $global; @@ -2652,21 +4917,58 @@ class API extends PluginAbstract } /** + * Create or delete a scheduled meeting (Meet plugin required). * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * 'RoomTopic' The meet title - * ['id' the meet_schedule_id to delete] - * ['starts' when the meet will start, the default is now] - * ['status' a= active | i = inactive] - * ['public' 2 = public, 1 = (Private) Logged Users Only, 0 = (Private) Specific User Groups [default value is 2]] - * ['userGroups' user groups array] - * ['RoomPasswordNew' the meet password] - * ['encodedPass' tell the script id the password submitted is raw or encrypted] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true&RoomTopic=APITestMeet + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - 'RoomTopic' (string, required): The title of the meeting. + * - ['id'] (int, optional): If provided, deletes the meeting with this ID. + * - ['starts'] (string, optional): Start datetime of the meeting (default is now). + * - ['status'] (string, optional): 'a' for active, 'i' for inactive. + * - ['public'] (int, optional): 2 = public, 1 = logged-in users only, 0 = specific user groups (default: 2). + * - ['userGroups'] (array, optional): Array of user group IDs. + * - ['RoomPasswordNew'] (string, optional): Meeting password. + * - ['encodedPass'] (bool, optional): If true, treats the password as already encoded. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true&RoomTopic=MyMeeting + * * @return string + * - On success: returns meeting data or confirmation of deletion. + * - On error: returns descriptive message. + * + * Notes: + * - Requires Meet plugin enabled. + * - User must have permission to create meetings. + * - If 'id' is passed, the function will delete the meeting instead of creating one. */ + #[OA\Post( + path: "/api/meet", + summary: "Create or delete a scheduled meeting", + description: "Creates or deletes a meeting depending on whether the 'id' is passed. Requires Meet plugin and user permissions.", + tags: ["Meet"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean")), + new OA\Parameter(name: "RoomTopic", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "id", in: "query", required: false, schema: new OA\Schema(type: "integer")), + new OA\Parameter(name: "starts", in: "query", required: false, schema: new OA\Schema(type: "string", format: "date-time")), + new OA\Parameter(name: "status", in: "query", required: false, schema: new OA\Schema(type: "string", enum: ["a", "i"])), + new OA\Parameter(name: "public", in: "query", required: false, schema: new OA\Schema(type: "integer", enum: [0, 1, 2])), + new OA\Parameter(name: "userGroups", in: "query", required: false, schema: new OA\Schema(type: "array", items: new OA\Items(type: "integer"))), + new OA\Parameter(name: "RoomPasswordNew", in: "query", required: false, schema: new OA\Schema(type: "string")) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Meeting created or deleted successfully" + ) + ] + )] + public function set_api_meet($parameters) { global $global; @@ -2685,14 +4987,45 @@ class API extends PluginAbstract } /** + * Return user notifications and current live streaming stats. * * @param array $parameters - * 'user' username of the user - * 'pass' password of the user - * ['encodedPass' tell the script id the password submitted is raw or encrypted] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b&encodedPass=true + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * - ['encodedPass'] (bool, optional): If true, indicates the password is already encrypted. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=abc123&encodedPass=true + * * @return string + * - JSON-encoded response containing: + * - notifications (from UserNotifications plugin) + * - live (from Live plugin stats) + * + * Notes: + * - Requires the **UserNotifications** plugin enabled. + * - Also fetches live stream stats via `Live/stats.json.php`. + * - Returns an error if the plugin is not enabled. */ + #[OA\Get( + path: "/api/notifications", + summary: "Get Notifications", + description: "Returns user notifications and live stream statistics. Requires the UserNotifications plugin.", + tags: ["API"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "encodedPass", in: "query", required: false, schema: new OA\Schema(type: "boolean")) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Successful response with notifications and live stats" + ) + ] + )] + public function get_api_notifications($parameters) { global $global; @@ -2712,12 +5045,43 @@ class API extends PluginAbstract /** + * Return the Roku-compatible JSON feed for the app. + * * @param array $parameters - * get the roku json - * 'APISecret' to list all videos - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} + * - 'APISecret' (string, required): Required to access all videos. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} + * * @return \ApiObject + * - Returns an object with the following structure: + * - providerName: Website title + * - language: Feed language (default: 'en') + * - lastUpdated: ISO 8601 date string + * - Sections with video entries (from Gallery or YouPHPFlix2 plugin) + * - cache: Whether the response was newly cached + * - cached: Boolean indicating if the response was served from cache + * + * Notes: + * - Uses `YouPHPFlix2` plugin if enabled; falls back to `Gallery`. + * - Caches result globally for 1 hour. + * - Requires `rowToRoku()` function to format video rows. */ + #[OA\Get( + path: "/api/app", + summary: "Get App Roku-compatible JSON feed", + description: "Returns Roku-compatible JSON feed based on YouPHPFlix2 or Gallery plugin. Requires a valid APISecret.", + tags: ["API"], + security: [ ['APISecret' => []] ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Roku JSON feed with sections and videos" + ) + ] + )] + public function get_api_app($parameters) { global $global, $config; @@ -2765,16 +5129,42 @@ class API extends PluginAbstract * @param array $parameters * * Generates a one-time login code for a specific user. - * The function takes username and password as parameters and creates a unique login code. - * This code is then saved into a log file within a specified directory. - * The code along with user details is encrypted before storing. - * The code expires in 10 minutes * - * ['user' username of the user] - * ['pass' password of the user] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b - * @return \ApiObject Returns an ApiObject containing the encrypted user information, including the generated code. + * Description: + * - Accepts a username and password. + * - Generates a unique login code (one-time use). + * - Stores the encrypted code and user data in a log file. + * - The code expires after 10 minutes. + * + * Parameters: + * - 'user' (string, required): Username of the user. + * - 'pass' (string, required): Password of the user. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&user=admin&pass=f321d14cdeeb7cded7489f504fa8862b + * + * @return \ApiObject + * - success: true if code generated + * - response: contains 'code', 'user', 'bytes' and other encrypted user data */ + #[OA\Post( + path: "/api/login_code", + summary: "Generate one-time login code", + description: "Generates a one-time use login code (valid for 10 minutes) for a given user and returns encrypted data.", + tags: ["Users"], + parameters: [ + new OA\Parameter(name: "user", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Username of the user."), + new OA\Parameter(name: "pass", in: "query", required: true, schema: new OA\Schema(type: "string"), description: "Password of the user.") + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Generated login code and encrypted payload" + ) + ] + )] + public function set_api_login_code($parameters) { $obj = getActivationCode(); @@ -2785,13 +5175,41 @@ class API extends PluginAbstract * @param array $parameters * * Verifies a one-time login code. - * The function takes the one-time login code as a parameter. It then fetches and decrypts the associated user information from the log file. - * If the file or the code does not exist, or the decrypted information is empty, it returns a message indicating the error. * - * 'code' The one-time login code generated in the set_api_login_code function. - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&code=XXXX-XXXX - * @return \ApiObject Returns an ApiObject containing the decrypted user information, or a message indicating the error. + * Description: + * - Takes a one-time login code as input. + * - Attempts to locate and decrypt the corresponding log file. + * - If valid, returns the user’s data. + * - The login code is valid for a limited time and is deleted after use. + * + * Parameters: + * - 'code' (string, required): The login code previously generated by `set_api_login_code`. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&code=XXXX-XXXX + * + * @return \ApiObject + * - success: true if the code is valid and not expired + * - response: object containing user ID, name, email, photo URL, and user hash + * - error: returns appropriate message if the code is invalid, expired, not found, or corrupted */ + #[OA\Get( + path: "/api/login_code", + summary: "Get Login Code", + description: "Verifies a one-time login code and returns associated user data.", + tags: ["Users"], + parameters: [ + new OA\Parameter(name: "code", in: "query", required: true, schema: new OA\Schema(type: "string")) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns user data if code is valid" + ) + ] + )] + public function get_api_login_code($parameters) { global $global, $config; @@ -2831,12 +5249,43 @@ class API extends PluginAbstract /** * @param array $parameters - * 'birth_date' The birth date in Y-m-d format. - * ['user' username of the user] - * ['pass' password of the user] - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&birth_date=1997-06-17 - * @return \ApiObject Returns an ApiObject. + * + * Updates the birth date of the currently logged-in user. + * + * Required Parameters: + * - 'birth_date' (string): The user's birth date in Y-m-d format (e.g., "1997-06-17"). + * + * Optional Authentication: + * - 'user' (string): Username for authentication (if not already logged in). + * - 'pass' (string): Password for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&birth_date=1997-06-17 + * + * @return \ApiObject + * - success: true if the birth date is updated successfully. + * - response: object containing the `users_id` and `error` flag. + * - error: true if the user is not logged in or update fails. */ + #[OA\Post( + path: "/api/birth", + summary: "Set Birth Date", + description: "Updates the birth date of the currently logged-in user.", + tags: ["Users"], + parameters: [ + new OA\Parameter(name: "birth_date", in: "query", required: true, schema: new OA\Schema(type: "string", format: "date")), + new OA\Parameter(name: "user", in: "query", required: false, schema: new OA\Schema(type: "string")), + new OA\Parameter(name: "pass", in: "query", required: false, schema: new OA\Schema(type: "string")) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns status of birth date update" + ) + ] + )] + public function set_api_birth($parameters) { global $global; @@ -2859,11 +5308,40 @@ class API extends PluginAbstract /** * @param array $parameters - * 'users_id' users id from what user you want the response. - * 'APISecret' mandatory for security reasons - required - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1 - * @return \ApiObject Returns an ApiObject. + * + * Check if a user's email is verified. + * + * Required Parameters: + * - 'users_id' (int): The ID of the user you want to check. + * - 'APISecret' (string): Required for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - response: object with: + * - users_id: The user ID provided + * - email_verified: true if the user's email is verified, false otherwise + * - error: true if the APISecret or users_id is invalid or missing */ + #[OA\Get( + path: "/api/is_verified", + summary: "Check Email Verification", + description: "Checks if the user's email is verified. Requires APISecret.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter(name: "users_id", in: "query", required: true, schema: new OA\Schema(type: "integer")), + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns whether the email is verified" + ) + ] + )] + public function get_api_is_verified($parameters) { global $global; @@ -2883,11 +5361,45 @@ class API extends PluginAbstract /** * @param array $parameters - * 'users_id' users id from what user you want the response. - * 'APISecret' mandatory for security reasons - required - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1 - * @return \ApiObject Returns an ApiObject. + * + * Send a verification email to the specified user. + * + * Required Parameters: + * - 'users_id' (int): The ID of the user to whom the verification email should be sent. + * - 'APISecret' (string): Required for authentication. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&users_id=1&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - response: object with: + * - users_id: The user ID provided + * - sent: true if the email was sent successfully, false otherwise + * - error: true if APISecret is invalid or users_id is missing */ + #[OA\Post( + path: "/api/send_verification_email", + summary: "Send verification email", + description: "Send a verification email to the user using the given users_id. Requires APISecret.", + tags: ["Users"], + security: [ ['APISecret' => []] ], + parameters: [ + new OA\Parameter( + name: "users_id", + in: "query", + required: true, + schema: new OA\Schema(type: "integer") + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Operation successful" + ) + ] + )] + public function set_api_send_verification_email($parameters) { global $global; @@ -2907,6 +5419,9 @@ class API extends PluginAbstract public static function isAPISecretValid() { global $global; + if (empty($_REQUEST['APISecret'])) { + $_REQUEST['APISecret'] = getBearerToken(); + } if (!empty($_REQUEST['APISecret'])) { $dataObj = AVideoPlugin::getDataObject('API'); if (trim($dataObj->APISecret) === trim($_REQUEST['APISecret'])) { @@ -2918,11 +5433,33 @@ class API extends PluginAbstract } /** - * return true if the secret is valid and false if it is not - * 'APISecret' mandatory for security reasons - required - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret={APISecret} - * @return \ApiObject Returns an ApiObject. + * Check if the provided APISecret is valid. + * + * Required Parameters: + * - 'APISecret' (string): The secret key to validate. + * + * @example + * {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&APISecret=YOUR_SECRET + * + * @return \ApiObject + * - message: "APISecret is valid" or "APISecret is invalid" + * - error: false if valid, true if invalid */ + #[OA\Get( + path: "/api/isAPISecretValid", + summary: "Validate APISecret", + description: "Checks whether the provided APISecret is valid.", + tags: ["API"], + security: [ ['APISecret' => []] ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Returns if APISecret is valid or not" + ) + ] + )] + public function get_api_isAPISecretValid() { global $global; @@ -2934,21 +5471,76 @@ class API extends PluginAbstract } /** - * decrypt a string - * 'string' mandatory - * @example {webSiteRootURL}plugin/API/{getOrSet}.json.php?APIName={APIName}&string=stringEncryptedToDecrypt - * @return \ApiObject Returns an ApiObject. + * @OA\Get( + * path="/api/decrypt", + * summary="Decrypt a string encrypted by the system", + * tags={"API"}, + * @OA\Parameter( + * name="APIName", + * in="query", + * required=true, + * @OA\Schema(type="string"), + * description="The name of the API to call" + * ), + * @OA\Parameter( + * name="string", + * in="query", + * required=true, + * @OA\Schema(type="string"), + * description="The encrypted string to decrypt" + * ), + * @OA\Response( + * response=200, + * description="Decrypted string", + * @OA\JsonContent(ref="#/components/schemas/ApiObject") + * ) + * ) */ + #[OA\Get( + path: "/api/decryptString", + summary: "Decrypt an encrypted string", + description: "Decrypts a string encrypted by the system.", + tags: ["API"], + parameters: [ + new OA\Parameter( + name: "string", + in: "query", + required: true, + schema: new OA\Schema(type: "string"), + description: "The encrypted string to decrypt" + ) + ], + responses: [ + new OA\Response( + response: 200, + content: new OA\JsonContent(ref: "#/components/schemas/ApiObject"), + description: "Decrypted string" + ) + ] + )] + public function get_api_decryptString() { $string = decryptString($_REQUEST['string']); return new ApiObject($string, empty($string)); } -} - +}#[OA\Schema( + schema: "ApiObject", + title: "ApiObject", + description: "Standard API response structure", + required: ["error", "message", "response", "execution_time"], + properties: [ + new OA\Property(property: "error", type: "boolean", example: false), + new OA\Property(property: "message", type: "string", example: "Operation successful"), + new OA\Property(property: "response", type: "object"), + new OA\Property(property: "users_id", type: "integer", example: 1), + new OA\Property(property: "user_age", type: "integer", example: 30), + new OA\Property(property: "session_id", type: "string", example: "abcd1234"), + new OA\Property(property: "execution_time", type: "number", example: 0.123) + ] +)] class ApiObject { - public $error; public $message; public $response; @@ -2956,9 +5548,21 @@ class ApiObject public $users_id; public $user_age; public $session_id; + public $execution_time; // Time in seconds + // Internal property to store start time + protected $start_time; + + /** + * Constructor: Instantiates the ApiObject and records the start time. + * + * @param string $message Initial message. + * @param bool $error Initial error flag. + * @param mixed $response Initial response data. + */ public function __construct($message = "api not started or not found", $error = true, $response = []) { + // Clean up response if needed $response = cleanUpRowFromDatabase($response); $this->error = $error; @@ -2968,9 +5572,51 @@ class ApiObject $this->users_id = User::getId(); $this->user_age = User::getAge(); $this->session_id = session_id(); + // Record start time when the object is instantiated + $this->start_time = microtime(true); + } + + /** + * Finalize the ApiObject with optional updated data and computes execution time. + * + * @param string|null $message Updated message (optional). + * @param bool|null $error Updated error flag (optional). + * @param mixed|null $response Updated response data (optional). + */ + public function finalize($message = null, $error = null, $response = null) + { + if ($message !== null) { + $this->message = $message; + $this->msg = $message; + } + if ($error !== null) { + $this->error = $error; + } + if ($response !== null) { + $this->response = $response; + } + // Calculate execution time since instantiation (in seconds) + $this->execution_time = microtime(true) - $this->start_time; } } + +#[OA\Schema( + schema: "SectionFirstPage", + title: "SectionFirstPage", + description: "Structure defining a section of the first page, with endpoints and execution metadata.", + required: ["type", "title", "endpoint", "rowCount"], + properties: [ + new OA\Property(property: "type", type: "string", example: "SubCategory"), + new OA\Property(property: "title", type: "string", example: "Movies"), + new OA\Property(property: "endpoint", type: "string", example: "https://example.com/api/videos"), + new OA\Property(property: "nextEndpoint", type: "string", example: "https://example.com/api/videos?current=2"), + new OA\Property(property: "rowCount", type: "integer", example: 10), + new OA\Property(property: "totalRows", type: "integer", example: 100), + new OA\Property(property: "childs", type: "array", items: new OA\Items(type: "object")), + new OA\Property(property: "executionTime", type: "number", format: "float", example: 0.235) + ] +)] class SectionFirstPage { diff --git a/plugin/API/docs/README.md b/plugin/API/docs/README.md new file mode 100644 index 0000000000..9b42682eaa --- /dev/null +++ b/plugin/API/docs/README.md @@ -0,0 +1,26 @@ +# Swagger UI + +Welcome to the Swagger UI documentation! + +## Usage + +- [Installation](usage/installation.md) +- [Configuration](usage/configuration.md) +- [CORS](usage/cors.md) +- [OAuth2](usage/oauth2.md) +- [Deep Linking](usage/deep-linking.md) +- [Limitations](usage/limitations.md) +- [Version detection](usage/version-detection.md) + +## Customization + +- [Overview](customization/overview.md) +- [Plugin API](customization/plugin-api.md) +- [Custom layout](customization/custom-layout.md) +- [Adding plugin](customization/add-plugin.md) +- [Plug-Points](customization/plug-points.md) + +## Development + +- [Setting up](development/setting-up.md) +- [Scripts](development/scripts.md) diff --git a/plugin/API/docs/book.json b/plugin/API/docs/book.json new file mode 100644 index 0000000000..3536cdff2c --- /dev/null +++ b/plugin/API/docs/book.json @@ -0,0 +1,3 @@ +{ + "title": "Swagger UI" +} diff --git a/plugin/API/docs/customization/add-plugin.md b/plugin/API/docs/customization/add-plugin.md new file mode 100644 index 0000000000..9ac03a5f0b --- /dev/null +++ b/plugin/API/docs/customization/add-plugin.md @@ -0,0 +1,127 @@ +# Add a plugin + +### Swagger-UI relies on plugins for all the good stuff. + +Plugins allow you to add +- `statePlugins` + - `selectors` - query the state + - `reducers` - modify the state + - `actions` - fire and forget, that will eventually be handled by a reducer. You *can* rely on the result of async actions. But in general it's not recommended + - `wrapActions` - replace an action with a wrapped action (useful for hooking into existing `actions`) +- `components` - React components +- `fn` - commons functions + +To add a plugin we include it in the configs... + +```js +SwaggerUI({ + url: 'some url', + plugins: [ ... ] +}) +``` + +Or if you're updating the core plugins.. you'll add it to the base preset: [src/core/presets/base/index.js](https://github.com/swagger-api/swagger-ui/blob/master/src/core/presets/base/index.js) + +Each Plugin is a function that returns an object. That object will get merged with the `system` and later bound to the state. +Here is an example of each `type` + +```js +// A contrived, but quite full example.... + +export function SomePlugin(toolbox) { + + const UPDATE_SOMETHING = "some_namespace_update_something" // strings just need to be uniuqe... see below + + // Tools + const fromJS = toolbox.Im.fromJS // needed below + const createSelector = toolbox.createSelector // same, needed below + + return { + statePlugins: { + + someNamespace: { + actions: { + actionName: (args)=> ({type: UPDATE_SOMETHING, payload: args}), // Synchronous action must return an object for the reducer to handle + anotherAction: (a,b,c) => (system) => system.someNamespaceActions.actionName(a || b) // Asynchronous actions must return a function. The function gets the whole system, and can call other actions (based on state if needed) + }, + wrapActions: { + anotherAction: (oriAction, system) => (...args) => { + oriAction(...args) // Usually we at least call the original action + system.someNamespace.actionName(...args) // why not call this? + console.log("args", args) // Log the args + // anotherAction in the someNamespace has now been replaced with the this function + } + }, + reducers: { + [UPDATE_SOMETHING]: (state, action) => { // Take a state (which is immutable) and an action (see synchronous actions) and return a new state + return state.set("something", fromJS(action.payload)) // we're updating the Immutable state object... we need to convert vanilla objects into an immutable type (fromJS) + // See immutable about how to modify the state + // PS: you're only working with the state under the namespace, in this case "someNamespace". So you can do what you want, without worrying about /other/ namespaces + } + }, + selectors: { + // creatSelector takes a list of fn's and passes all the results to the last fn. + // eg: createSelector(a => a, a => a+1, (a,a2) => a + a2)(1) // = 3 + something: createSelector( // see [reselect#createSelector](https://github.com/reactjs/reselect#createselectorinputselectors--inputselectors-resultfunc) + getState => getState(), // This is a requirement... because we `bind` selectors, we don't want to bind to any particular state (which is an immutable value) so we bind to a function, which returns the current state + state => state.get("something") // return the whatever "something" points to + ), + foo: getState => "bar" // In the end selectors are just functions that we pass getState to + } + } + + ... // you can include as many namespaces as you want. They just get merged into the 'system' + + }, + + components: { + foo: ()=>

Hello

// just a map of names to react components, naturally you'd want to import a fuller react component + }, + + fn: { + addOne: (a) => a + 1 // just any extra functions you want to include + } + } +} +``` + +>The plugin factory gets one argument, which I like to call `toolbox`. +This argument is the entire plugin system (at the point the plugin factory is called). It also includes a reference to the `Immutable` lib, so that plugin authors don't need to include it. + + +### The Plugin system + +Each plugin you include will end up getting merged into the `system`, which is just an object. + +Then we bind the `system` to our state. And flatten it, so that we don't need to reach into deep objects + +> ie: spec.actions becomes specActions, spec.selectors becomes specSelectors + +You can reach this bound system by calling `getSystem` on the store. + +`getSystem` is the heart of this whole project. Each container component will receive a spread of props from `getSystem` + +here is an example.... +```js +class Bobby extends React.Component { + + handleClick(e) { + this.props.someNamespaceActions.actionName() // fires an action... which the reducer will *eventually* see + } + + render() { + + let { someNamespaceSelectors, someNamespaceActions } = this.props // this.props has the whole state spread + let something = someNamespaceSelectors.something() // calls our selector, which returns some state (either an immutable object or value) + + return ( +

Hello {something}

// render the contents + ) + + } + +} +``` + +TODO: a lot more elaboration +` diff --git a/plugin/API/docs/customization/custom-layout.md b/plugin/API/docs/customization/custom-layout.md new file mode 100644 index 0000000000..d54133520e --- /dev/null +++ b/plugin/API/docs/customization/custom-layout.md @@ -0,0 +1,92 @@ +# Creating a custom layout + +**Layouts** are a special type of component that Swagger UI uses as the root component for the entire application. You can define custom layouts in order to have high-level control over what ends up on the page. + +By default, Swagger UI uses `BaseLayout`, which is built into the application. You can specify a different layout to be used by passing the layout's name as the `layout` parameter to Swagger UI. Be sure to provide your custom layout as a component to Swagger UI. + +
+ +For example, if you wanted to create a custom layout that only displayed operations, you could define an `OperationsLayout`: + +```js +import React from "react" + +// Create the layout component +class OperationsLayout extends React.Component { + render() { + const { + getComponent + } = this.props + + const Operations = getComponent("operations", true) + + return ( +
+ +
+ ) + } +} + +// Create the plugin that provides our layout component +const OperationsLayoutPlugin = () => { + return { + components: { + OperationsLayout: OperationsLayout + } + } +} + +// Provide the plugin to Swagger-UI, and select OperationsLayout +// as the layout for Swagger-UI +SwaggerUI({ + url: "https://petstore.swagger.io/v2/swagger.json", + plugins: [ OperationsLayoutPlugin ], + layout: "OperationsLayout" +}) +``` + +### Augmenting the default layout + +If you'd like to build around the `BaseLayout` instead of replacing it, you can pull the `BaseLayout` into your custom layout and use it: + +```js +import React from "react" + +// Create the layout component +class AugmentingLayout extends React.Component { + render() { + const { + getComponent + } = this.props + + const BaseLayout = getComponent("BaseLayout", true) + + return ( +
+
+

I have a custom header above Swagger-UI!

+
+ +
+ ) + } +} + +// Create the plugin that provides our layout component +const AugmentingLayoutPlugin = () => { + return { + components: { + AugmentingLayout: AugmentingLayout + } + } +} + +// Provide the plugin to Swagger-UI, and select AugmentingLayout +// as the layout for Swagger-UI +SwaggerUI({ + url: "https://petstore.swagger.io/v2/swagger.json", + plugins: [ AugmentingLayoutPlugin ], + layout: "AugmentingLayout" +}) +``` diff --git a/plugin/API/docs/customization/overview.md b/plugin/API/docs/customization/overview.md new file mode 100644 index 0000000000..4979804463 --- /dev/null +++ b/plugin/API/docs/customization/overview.md @@ -0,0 +1,71 @@ +# Plugin system overview + +### Prior art + +Swagger UI leans heavily on concepts and patterns found in React and Redux. + +If you aren't already familiar, here's some suggested reading: + +- [React: Quick Start (reactjs.org)](https://reactjs.org/docs/hello-world.html) +- [Redux README (redux.js.org)](https://redux.js.org/) + +In the following documentation, we won't take the time to define the fundamentals covered in the resources above. + +> **Note**: Some of the examples in this section contain JSX, which is a syntax extension to JavaScript that is useful for writing React components. +> +> If you don't want to set up a build pipeline capable of translating JSX to JavaScript, take a look at [React without JSX (reactjs.org)](https://reactjs.org/docs/react-without-jsx.html). You can use our `system.React` reference to leverage React without needing to pull a copy into your project. + +### The System + +The _system_ is the heart of the Swagger UI application. At runtime, it's a JavaScript object that holds many things: + +- React components +- Bound Redux actions and reducers +- Bound Reselect state selectors +- System-wide collection of available components +- Built-in helpers like `getComponent`, `makeMappedContainer`, and `getStore` +- References to the React and Immutable.js libraries (`system.React`, `system.Im`) +- User-defined helper functions + +The system is built up when Swagger UI is called by iterating through ("compiling") each plugin that Swagger UI has been given, through the `presets` and `plugins` configuration options. + +### Presets + +Presets are arrays of plugins, which are provided to Swagger UI through the `presets` configuration option. All plugins within presets are compiled before any plugins provided via the `plugins` configuration option. Consider the following example: + +```javascript +const MyPreset = [FirstPlugin, SecondPlugin, ThirdPlugin] + +SwaggerUI({ + presets: [ + MyPreset + ] +}) +``` + +By default, Swagger UI includes the internal `ApisPreset`, which contains a set of plugins that provide baseline functionality for Swagger UI. If you specify your own `presets` option, you need to add the ApisPreset manually, like so: + +```javascript +SwaggerUI({ + presets: [ + SwaggerUI.presets.apis, + MyAmazingCustomPreset + ] +}) +``` + +The need to provide the `apis` preset when adding other presets is an artifact of Swagger UI's original design, and will likely be removed in the next major version. + +### getComponent + +`getComponent` is a helper function injected into every container component, which is used to get references to components provided by the plugin system. + +All components should be loaded through `getComponent`, since it allows other plugins to modify the component. It is preferred over a conventional `import` statement. + +Container components in Swagger UI can be loaded by passing `true` as the second argument to `getComponent`, like so: + +```javascript +getComponent("ContainerComponentName", true) +``` + +This will map the current system as props to the component. diff --git a/plugin/API/docs/customization/plug-points.md b/plugin/API/docs/customization/plug-points.md new file mode 100644 index 0000000000..f687e4642f --- /dev/null +++ b/plugin/API/docs/customization/plug-points.md @@ -0,0 +1,415 @@ +# Plug points + +Swagger UI exposes most of its internal logic through the plugin system. + +Often, it is beneficial to override the core internals to achieve custom behavior. + +### Note: Semantic Versioning + +Swagger UI's internal APIs are _not_ part of our public contract, which means that they can change without the major version change. + +If your custom plugins wrap, extend, override, or consume any internal core APIs, we recommend specifying a specific minor version of Swagger UI to use in your application, because they will _not_ change between patch versions. + +If you're installing Swagger UI via NPM, for example, you can do this by using a tilde: + +```js +{ + "dependencies": { + "swagger-ui": "~3.11.0" + } +} +``` + +### `fn.opsFilter` + +When using the `filter` option, tag names will be filtered by the user-provided value. If you'd like to customize this behavior, you can override the default `opsFilter` function. + +For example, you can implement a multiple-phrase filter: + +```js +const MultiplePhraseFilterPlugin = function() { + return { + fn: { + opsFilter: (taggedOps, phrase) => { + const phrases = phrase.split(", ") + + return taggedOps.filter((val, key) => { + return phrases.some(item => key.indexOf(item) > -1) + }) + } + } + } +} +``` +### Logo component +While using the Standalone Preset the SwaggerUI logo is rendered in the Top Bar. +The logo can be exchanged by replacing the `Logo` component via the plugin api: + +```jsx +import React from "react"; +const MyLogoPlugin = { + components: { + Logo: () => ( + My Logo + ) + } +} +``` + + +### JSON Schema components +In swagger there are so called JSON Schema components. These are used to render inputs for parameters and components of request bodies with `application/x-www-form-urlencoded` or `multipart/*` media-type. + +Internally swagger uses following mapping to find the JSON Schema component from OpenAPI Specification schema information: + +For each schema’s type(eg. `string`, `array`, …) and if defined schema’s format (eg. ‘date’, ‘uuid’, …) there is a corresponding component mapping: + +**If format defined:** +```js +`JsonSchema_${type}_${format}` +``` + +**Fallback if `JsonSchema_${type}_${format}` component does not exist or format not defined:** +```js +`JsonSchema_${type}` +``` + +**Default:** +```js +`JsonSchema_string` +``` + +With this, one can define custom input components or override existing. + +#### Example Date-Picker plugin + +If one would like to input date values you could provide a custom plugin to integrate [react-datepicker](https://www.npmjs.com/package/react-datepicker) into swagger-ui. +All you need to do is to create a component to wrap [react-datepicker](https://www.npmjs.com/package/react-datepicker) accordingly to the format. + +**There are two cases:** +- ```yaml + type: string + format: date + ``` + The resulting name for mapping to succeed: `JsonSchema_string_date` +- ```yaml + type: string + format: date-time + ``` + The resulting name for mapping to succeed: `JsonSchema_string_date-time` + +This creates the need for two components and simple logic to strip any time input in case the format is date: +```js +import React from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; + +const JsonSchema_string_date = (props) => { + const dateNumber = Date.parse(props.value); + const date = dateNumber + ? new Date(dateNumber) + : new Date(); + + return ( + props.onChange(d.toISOString().substring(0, 10))} + /> + ); +} + +const JsonSchema_string_date_time = (props) => { + const dateNumber = Date.parse(props.value); + const date = dateNumber + ? new Date(dateNumber) + : new Date(); + + return ( + props.onChange(d.toISOString())} + showTimeSelect + timeFormat="p" + dateFormat="Pp" + /> + ); +} + + +export const DateTimeSwaggerPlugin = { + components: { + JsonSchema_string_date: JsonSchema_string_date, + "JsonSchema_string_date-time": JsonSchema_string_date_time + } +}; +``` + +### Request Snippets + +SwaggerUI can be configured with the `requestSnippetsEnabled: true` option to activate Request Snippets. +Instead of the generic curl that is generated upon doing a request. It gives you more granular options: +- curl for bash +- curl for cmd +- curl for powershell + +There might be the case where you want to provide your own snipped generator. This can be done by using the plugin api. +A Request Snipped generator consists of the configuration and a `fn`, +which takes the internal request object and transforms it to the desired snippet. + +```js +// Add config to Request Snippets Configuration with an unique key like "node_native" +const snippetConfig = { + requestSnippetsEnabled: true, + requestSnippets: { + generators: { + "node_native": { + title: "NodeJs Native", + syntax: "javascript" + } + } + } +} + +const SnippedGeneratorNodeJsPlugin = { + fn: { + // use `requestSnippetGenerator_` + key from config (node_native) for generator fn + requestSnippetGenerator_node_native: (request) => { + const url = new Url(request.get("url")) + let isMultipartFormDataRequest = false + const headers = request.get("headers") + if(headers && headers.size) { + request.get("headers").map((val, key) => { + isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(key) && /^multipart\/form-data$/i.test(val) + }) + } + const packageStr = url.protocol === "https:" ? "https" : "http" + let reqBody = request.get("body") + if (request.get("body")) { + if (isMultipartFormDataRequest && ["POST", "PUT", "PATCH"].includes(request.get("method"))) { + return "throw new Error(\"Currently unsupported content-type: /^multipart\\/form-data$/i\");" + } else { + if (!Map.isMap(reqBody)) { + if (typeof reqBody !== "string") { + reqBody = JSON.stringify(reqBody) + } + } else { + reqBody = getStringBodyOfMap(request) + } + } + } else if (!request.get("body") && request.get("method") === "POST") { + reqBody = "" + } + + const stringBody = "`" + (reqBody || "") + .replace(/\\n/g, "\n") + .replace(/`/g, "\\`") + + "`" + + return `const http = require("${packageStr}"); +const options = { + "method": "${request.get("method")}", + "hostname": "${url.host}", + "port": ${url.port || "null"}, + "path": "${url.pathname}"${headers && headers.size ? `, + "headers": { + ${request.get("headers").map((val, key) => `"${key}": "${val}"`).valueSeq().join(",\n ")} + }` : ""} +}; +const req = http.request(options, function (res) { + const chunks = []; + res.on("data", function (chunk) { + chunks.push(chunk); + }); + res.on("end", function () { + const body = Buffer.concat(chunks); + console.log(body.toString()); + }); +}); +${reqBody ? `\nreq.write(${stringBody});` : ""} +req.end();` + } + } +} + +const ui = SwaggerUIBundle({ + "dom_id": "#swagger-ui", + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl, + SnippedGeneratorNodeJsPlugin + ], + layout: "StandaloneLayout", + validatorUrl: "https://validator.swagger.io/validator", + url: "https://petstore.swagger.io/v2/swagger.json", + ...snippetConfig, +}) +``` + +### Error handling + +SwaggerUI comes with a `safe-render` plugin that handles error handling allows plugging into error handling system and modify it. + +The plugin accepts a list of component names that should be protected by error boundaries. + +Its public API looks like this: + +```js +{ + fn: { + componentDidCatch, + withErrorBoundary: withErrorBoundary(getSystem), + }, + components: { + ErrorBoundary, + Fallback, + }, +} +``` + +safe-render plugin is automatically utilized by [base](https://github.com/swagger-api/swagger-ui/blob/78f62c300a6d137e65fd027d850981b010009970/src/core/presets/base.js) and [standalone](https://github.com/swagger-api/swagger-ui/tree/78f62c300a6d137e65fd027d850981b010009970/src/standalone) SwaggerUI presets and +should always be used as the last plugin, after all the components are already known to the SwaggerUI. +The plugin defines a default list of components that should be protected by error boundaries: + +```js +[ + "App", + "BaseLayout", + "VersionPragmaFilter", + "InfoContainer", + "ServersContainer", + "SchemesContainer", + "AuthorizeBtnContainer", + "FilterContainer", + "Operations", + "OperationContainer", + "parameters", + "responses", + "OperationServers", + "Models", + "ModelWrapper", + "Topbar", + "StandaloneLayout", + "onlineValidatorBadge" +] +``` + +As demonstrated below, additional components can be protected by utilizing the safe-render plugin +with configuration options. This gets really handy if you are a SwaggerUI integrator and you maintain a number of +plugins with additional custom components. + +```js +const swaggerUI = SwaggerUI({ + url: "https://petstore.swagger.io/v2/swagger.json", + dom_id: '#swagger-ui', + plugins: [ + () => ({ + components: { + MyCustomComponent1: () => 'my custom component', + }, + }), + SwaggerUI.plugins.SafeRender({ + fullOverride: true, // only the component list defined here will apply (not the default list) + componentList: [ + "MyCustomComponent1", + ], + }), + ], +}); +``` + +##### componentDidCatch + +This static function is invoked after a component has thrown an error. +It receives two parameters: + +1. `error` - The error that was thrown. +2. `info` - An object with a componentStack key containing [information about which component threw the error](https://reactjs.org/docs/error-boundaries.html#component-stack-traces). + +It has precisely the same signature as error boundaries [componentDidCatch lifecycle method](https://reactjs.org/docs/react-component.html#componentdidcatch), +except it's a static function and not a class method. + +Default implement of componentDidCatch uses `console.error` to display the received error: + +```js +export const componentDidCatch = console.error; +``` + +To utilize your own error handling logic (e.g. [bugsnag](https://www.bugsnag.com/)), create new SwaggerUI plugin that overrides componentDidCatch: + +{% highlight js linenos %} +const BugsnagErrorHandlerPlugin = () => { + // init bugsnag + + return { + fn: { + componentDidCatch = (error, info) => { + Bugsnag.notify(error); + Bugsnag.notify(info); + }, + }, + }; +}; +{% endhighlight %} + +##### withErrorBoundary + +This function is HOC (Higher Order Component). It wraps a particular component into the `ErrorBoundary` component. +It can be overridden via a plugin system to control how components are wrapped by the ErrorBoundary component. +In 99.9% of situations, you won't need to override this function, but if you do, please read the source code of this function first. + +##### Fallback + +The component is displayed when the error boundary catches an error. It can be overridden via a plugin system. +Its default implementation is trivial: + +```js +import React from "react" +import PropTypes from "prop-types" + +const Fallback = ({ name }) => ( +
+ 😱 Could not render { name === "t" ? "this component" : name }, see the console. +
+) +Fallback.propTypes = { + name: PropTypes.string.isRequired, +} +export default Fallback +``` + +Feel free to override it to match your look & feel: + +```js +const CustomFallbackPlugin = () => ({ + components: { + Fallback: ({ name } ) => `This is my custom fallback. ${name} failed to render`, + }, +}); + +const swaggerUI = SwaggerUI({ + url: "https://petstore.swagger.io/v2/swagger.json", + dom_id: '#swagger-ui', + plugins: [ + CustomFallbackPlugin, + ] +}); +``` + +##### ErrorBoundary + +This is the component that implements React error boundaries. Uses `componentDidCatch` and `Fallback` +under the hood. In 99.9% of situations, you won't need to override this component, but if you do, +please read the source code of this component first. + + +##### Change in behavior + +In prior releases of SwaggerUI (before v4.3.0), almost all components have been protected, and when thrown error, +`Fallback` component was displayed. This changes with SwaggerUI v4.3.0. Only components defined +by the `safe-render` plugin are now protected and display fallback. If a small component somewhere within +SwaggerUI React component tree fails to render and throws an error. The error bubbles up to the closest +error boundary, and that error boundary displays the `Fallback` component and invokes `componentDidCatch`. diff --git a/plugin/API/docs/customization/plugin-api.md b/plugin/API/docs/customization/plugin-api.md new file mode 100644 index 0000000000..d491d7e55d --- /dev/null +++ b/plugin/API/docs/customization/plugin-api.md @@ -0,0 +1,455 @@ +# Plugins + +A plugin is a function that returns an object - more specifically, the object may contain functions and components that augment and modify Swagger UI's functionality. + +### Note: Semantic Versioning + +Swagger UI's internal APIs are _not_ part of our public contract, which means that they can change without the major version change. + +If your custom plugins wrap, extend, override, or consume any internal core APIs, we recommend specifying a specific minor version of Swagger UI to use in your application, because they will _not_ change between patch versions. + +If you're installing Swagger UI via NPM, for example, you can do this by using a tilde: + +```js +{ + "dependencies": { + "swagger-ui": "~3.11.0" + } +} +``` + +### Format + +A plugin return value may contain any of these keys, where `stateKey` is a name for a piece of state: + +```javascript +{ + statePlugins: { + [stateKey]: { + actions, + reducers, + selectors, + wrapActions, + wrapSelectors + } + }, + components: {}, + wrapComponents: {}, + rootInjects: {}, + afterLoad: (system) => {}, + fn: {}, +} +``` + +### System is provided to plugins + +Let's assume we have a plugin, `NormalPlugin`, that exposes a `doStuff` action under the `normal` state namespace. + +```javascript +const ExtendingPlugin = function(system) { + return { + statePlugins: { + extending: { + actions: { + doExtendedThings: function(...args) { + // you can do other things in here if you want + return system.normalActions.doStuff(...args) + } + } + } + } + } +} +``` + +As you can see, each plugin is passed a reference to the `system` being built up. As long as `NormalPlugin` is compiled before `ExtendingPlugin`, this will work without any issues. + +There is no dependency management built into the plugin system, so if you create a plugin that relies on another, it is your responsibility to make sure that the dependent plugin is loaded _after_ the plugin being depended on. + +### Interfaces + +#### Actions + +```javascript +const MyActionPlugin = () => { + return { + statePlugins: { + example: { + actions: { + updateFavoriteColor: (str) => { + return { + type: "EXAMPLE_SET_FAV_COLOR", + payload: str + } + } + } + } + } + } +} +``` + +Once an action has been defined, you can use it anywhere that you can get a system reference: + +```javascript +// elsewhere +system.exampleActions.updateFavoriteColor("blue") +``` + +The Action interface enables the creation of new Redux action creators within a piece of state in the Swagger UI system. + +This action creator function will be exposed to container components as `exampleActions.updateFavoriteColor`. When this action creator is called, the return value (which should be a [Flux Standard Action](https://github.com/acdlite/flux-standard-action)) will be passed to the `example` reducer, which we'll define in the next section. + +For more information about the concept of actions in Redux, see the [Redux Actions documentation](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#actions). + +#### Reducers + +Reducers take a state (which is an [Immutable.js map](https://facebook.github.io/immutable-js/docs/#/Map)) and an action, then returns a new state. + +Reducers must be provided to the system under the name of the action type that they handle, in this case, `EXAMPLE_SET_FAV_COLOR`. + +```javascript +const MyReducerPlugin = function(system) { + return { + statePlugins: { + example: { + reducers: { + "EXAMPLE_SET_FAV_COLOR": (state, action) => { + // you're only working with the state under the namespace, in this case "example". + // So you can do what you want, without worrying about /other/ namespaces + return state.set("favColor", action.payload) + } + } + } + } + } +} +``` + +#### Selectors + +Selectors reach into their namespace's state to retrieve or derive data from the state. + +They're an easy way to keep logic in one place, and are preferred over passing state data directly into components. + + +```javascript +const MySelectorPlugin = function(system) { + return { + statePlugins: { + example: { + selectors: { + myFavoriteColor: (state) => state.get("favColor") + } + } + } + } +} +``` + +You can also use the Reselect library to memoize your selectors, which is recommended for any selectors that will see heavy use, since Reselect automatically memoizes selector calls for you: + +```javascript +import { createSelector } from "reselect" + +const MySelectorPlugin = function(system) { + return { + statePlugins: { + example: { + selectors: { + // this selector will be memoized after it is run once for a + // value of `state` + myFavoriteColor: createSelector((state) => state.get("favColor")) + } + } + } + } +} +``` + +Once a selector has been defined, you can use it anywhere that you can get a system reference: +```javascript +system.exampleSelectors.myFavoriteColor() // gets `favColor` in state for you +``` + +#### Components + +You can provide a map of components to be integrated into the system. + +Be mindful of the key names for the components you provide, as you'll need to use those names to refer to the components elsewhere. + +```javascript +class HelloWorldClass extends React.Component { + render() { + return

Hello World!

+ } +} + +const MyComponentPlugin = function(system) { + return { + components: { + HelloWorldClass: HelloWorldClass + // components can just be functions, these are called "stateless components" + HelloWorldStateless: () =>

Hello World!

, + } + } +} +``` + +```javascript +// elsewhere +const HelloWorldStateless = system.getComponent("HelloWorldStateless") +const HelloWorldClass = system.getComponent("HelloWorldClass") +``` + +You can also "cancel out" any components that you don't want by creating a stateless component that always returns `null`: + +```javascript +const NeverShowInfoPlugin = function(system) { + return { + components: { + info: () => null + } + } +} +``` + +You can use `config.failSilently` if you don't want a warning when a component doesn't exist in the system. + +Be mindful of `getComponent` arguments order. In the example below, the boolean `false` refers to presence of a container, and the 3rd argument is the config object used to suppress the missing component warning. +```javascript +const thisVariableWillBeNull = getComponent("not_real", false, { failSilently: true }) +``` + +#### Wrap-Actions + +Wrap Actions allow you to override the behavior of an action in the system. + +They are function factories with the signature `(oriAction, system) => (...args) => result`. + +A Wrap Action's first argument is `oriAction`, which is the action being wrapped. It is your responsibility to call the `oriAction` - if you don't, the original action will not fire! + +This mechanism is useful for conditionally overriding built-in behaviors, or listening to actions. + +```javascript +// FYI: in an actual Swagger UI, `updateSpec` is already defined in the core code +// it's just here for clarity on what's behind the scenes +const MySpecPlugin = function(system) { + return { + statePlugins: { + spec: { + actions: { + updateSpec: (str) => { + return { + type: "SPEC_UPDATE_SPEC", + payload: str + } + } + } + } + } + } +} + +// this plugin allows you to watch changes to the spec that is in memory +const MyWrapActionPlugin = function(system) { + return { + statePlugins: { + spec: { + wrapActions: { + updateSpec: (oriAction, system) => (str) => { + // here, you can hand the value to some function that exists outside of Swagger UI + console.log("Here is my API definition", str) + return oriAction(str) // don't forget! otherwise, Swagger UI won't update + } + } + } + } + } +} +``` + +#### Wrap-Selectors + +Wrap Selectors allow you to override the behavior of a selector in the system. + +They are function factories with the signature `(oriSelector, system) => (state, ...args) => result`. + +This interface is useful for controlling what data flows into components. We use this in the core code to disable selectors based on the API definition's version. + +```javascript +import { createSelector } from 'reselect' + +// FYI: in an actual Swagger UI, the `url` spec selector is already defined +// it's just here for clarity on what's behind the scenes +const MySpecPlugin = function(system) { + return { + statePlugins: { + spec: { + selectors: { + url: createSelector( + state => state.get("url") + ) + } + } + } + } +} + +const MyWrapSelectorsPlugin = function(system) { + return { + statePlugins: { + spec: { + wrapSelectors: { + url: (oriSelector, system) => (state, ...args) => { + console.log('someone asked for the spec url!!! it is', state.get('url')) + // you can return other values here... + // but let's just enable the default behavior + return oriSelector(state, ...args) + } + } + } + } + } +} +``` + +#### Wrap-Components + +Wrap Components allow you to override a component registered within the system. + +Wrap Components are function factories with the signature `(OriginalComponent, system) => props => ReactElement`. If you'd prefer to provide a React component class, `(OriginalComponent, system) => ReactClass` works as well. + +```javascript +const MyWrapBuiltinComponentPlugin = function(system) { + return { + wrapComponents: { + info: (Original, system) => (props) => { + return
+

Hello world! I am above the Info component.

+ +
+ } + } + } +} +``` + +Here's another example that includes a code sample of a component that will be wrapped: + +```javascript +///// Overriding a component from a plugin + +// Here's our normal, unmodified component. +const MyNumberDisplayPlugin = function(system) { + return { + components: { + NumberDisplay: ({ number }) => {number} + } + } +} + +// Here's a component wrapper defined as a function. +const MyWrapComponentPlugin = function(system) { + return { + wrapComponents: { + NumberDisplay: (Original, system) => (props) => { + if(props.number > 10) { + return
+

Warning! Big number ahead.

+ +
+ } else { + return + } + } + } + } +} + +// Alternatively, here's the same component wrapper defined as a class. +const MyWrapComponentPlugin = function(system) { + return { + wrapComponents: { + NumberDisplay: (Original, system) => class WrappedNumberDisplay extends React.component { + render() { + if(props.number > 10) { + return
+

Warning! Big number ahead.

+ +
+ } else { + return + } + } + } + } + } +} +``` + +#### `rootInjects` + +The `rootInjects` interface allows you to inject values at the top level of the system. + +This interface takes an object, which will be merged in with the top-level system object at runtime. + +```js +const MyRootInjectsPlugin = function(system) { + return { + rootInjects: { + myConstant: 123, + myMethod: (...params) => console.log(...params) + } + } +} +``` + +#### `afterLoad` + +The `afterLoad` plugin method allows you to get a reference to the system after your plugin has been registered. + +This interface is used in the core code to attach methods that are driven by bound selectors or actions. You can also use it to execute logic that requires your plugin to already be ready, for example fetching initial data from a remote endpoint and passing it to an action your plugin creates. + +The plugin context, which is bound to `this`, is undocumented, but below is an example of how to attach a bound action as a top-level method: + +```javascript +const MyMethodProvidingPlugin = function() { + return { + afterLoad(system) { + // at this point in time, your actions have been bound into the system + // so you can do things with them + this.rootInjects = this.rootInjects || {} + this.rootInjects.myMethod = system.exampleActions.updateFavoriteColor + }, + statePlugins: { + example: { + actions: { + updateFavoriteColor: (str) => { + return { + type: "EXAMPLE_SET_FAV_COLOR", + payload: str + } + } + } + } + } + } +} +``` + +#### fn + +The fn interface allows you to add helper functions to the system for use elsewhere. + +```javascript +import leftPad from "left-pad" + +const MyFnPlugin = function(system) { + return { + fn: { + leftPad: leftPad + } + } +} +``` diff --git a/plugin/API/docs/development/scripts.md b/plugin/API/docs/development/scripts.md new file mode 100644 index 0000000000..4c746e7f6a --- /dev/null +++ b/plugin/API/docs/development/scripts.md @@ -0,0 +1,40 @@ +# Helpful scripts + +Any of the scripts below can be run by typing `npm run + + + + diff --git a/plugin/API/docs/oauth2-redirect.html b/plugin/API/docs/oauth2-redirect.html new file mode 100644 index 0000000000..5640917181 --- /dev/null +++ b/plugin/API/docs/oauth2-redirect.html @@ -0,0 +1,79 @@ + + + + Swagger UI: OAuth2 Redirect + + + + + diff --git a/plugin/API/docs/samples/webpack-getting-started/README.md b/plugin/API/docs/samples/webpack-getting-started/README.md new file mode 100644 index 0000000000..df42fc02de --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/README.md @@ -0,0 +1,14 @@ + +### Demo of Swagger UI with Webpack. + +This `webpack-getting-started` sample is for reference only. + +It includes CSS and OAuth configuration. + +`_sample_package.json` is a placeholder sample. You should rename this file, per `Usage` section below, and you should also verify and update this sample's `@latest` compared to the `swagger-ui@latest` + + +#### Usage + rename `_sample_package.json` to `package.json` + npm install + npm start diff --git a/plugin/API/docs/samples/webpack-getting-started/_sample_package.json b/plugin/API/docs/samples/webpack-getting-started/_sample_package.json new file mode 100644 index 0000000000..74d65663e4 --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/_sample_package.json @@ -0,0 +1,26 @@ +{ + "name": "swagger-ui-webpack-getting-started", + "version": "0.0.1", + "description": "A simple setup of Swagger UI with Webpack", + "scripts": { + "build": "webpack", + "start": "webpack-dev-server --open" + }, + "author": "Shaun Luttin", + "license": "Apache-2.0", + "devDependencies": { + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^11.0.0", + "html-webpack-plugin": "^5.5.0", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.0" + }, + "dependencies": { + "css-loader": "^6.7.1", + "json-loader": "^0.5.7", + "style-loader": "^3.3.1", + "swagger-ui": "^4.14.0", + "yaml-loader": "^0.8.0" + } +} diff --git a/plugin/API/docs/samples/webpack-getting-started/index.html b/plugin/API/docs/samples/webpack-getting-started/index.html new file mode 100644 index 0000000000..bc3c517374 --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/index.html @@ -0,0 +1,10 @@ + + + + + Getting Started + + +
+ + diff --git a/plugin/API/docs/samples/webpack-getting-started/src/index.js b/plugin/API/docs/samples/webpack-getting-started/src/index.js new file mode 100644 index 0000000000..13e5862fe8 --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/src/index.js @@ -0,0 +1,15 @@ +import SwaggerUI from 'swagger-ui' +import 'swagger-ui/dist/swagger-ui.css'; + +const spec = require('./swagger-config.yaml'); + +const ui = SwaggerUI({ + spec, + dom_id: '#swagger', +}); + +ui.initOAuth({ + appName: "Swagger UI Webpack Demo", + // See https://demo.identityserver.io/ for configuration details. + clientId: 'implicit' +}); diff --git a/plugin/API/docs/samples/webpack-getting-started/src/swagger-config.yaml b/plugin/API/docs/samples/webpack-getting-started/src/swagger-config.yaml new file mode 100644 index 0000000000..5496a6026c --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/src/swagger-config.yaml @@ -0,0 +1,30 @@ +openapi: "3.0.4" +info: + version: "0.0.1" + title: "Swagger UI Webpack Setup" + description: "Demonstrates Swagger UI with Webpack including CSS and OAuth" +servers: + - url: "https://demo.identityserver.io/api" + description: "Identity Server test API" +components: + securitySchemes: + # See https://demo.identityserver.io/ for configuration details. + identity_server_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: "https://demo.identityserver.io/connect/authorize" + scopes: + api: "api" +security: + - identity_server_auth: + - api +paths: + /test: + get: + summary: "Runs a test request against the Identity Server demo API" + responses: + 401: + description: "Unauthorized" + 200: + description: "OK" diff --git a/plugin/API/docs/samples/webpack-getting-started/webpack.config.js b/plugin/API/docs/samples/webpack-getting-started/webpack.config.js new file mode 100644 index 0000000000..9ea2913f4b --- /dev/null +++ b/plugin/API/docs/samples/webpack-getting-started/webpack.config.js @@ -0,0 +1,52 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const outputPath = path.resolve(__dirname, 'dist'); + +module.exports = { + mode: 'development', + entry: { + app: require.resolve('./src/index'), + }, + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.yaml$/, + use: [ + { loader: 'json-loader' }, + { loader: 'yaml-loader', options:{ asJSON: true } } + ] + }, + { + test: /\.css$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader' }, + ] + } + ] + }, + plugins: [ + new CleanWebpackPlugin(), + new CopyWebpackPlugin({patterns:[ + { + // Copy the Swagger OAuth2 redirect file to the project root; + // that file handles the OAuth2 redirect after authenticating the end-user. + from: require.resolve('swagger-ui/dist/oauth2-redirect.html'), + to: './' + } + ]}), + new HtmlWebpackPlugin({ + template: 'index.html' + }) + ], + output: { + filename: '[name].bundle.js', + path: outputPath, + } +}; diff --git a/plugin/API/docs/swagger-initializer.js b/plugin/API/docs/swagger-initializer.js new file mode 100644 index 0000000000..015075bc01 --- /dev/null +++ b/plugin/API/docs/swagger-initializer.js @@ -0,0 +1,21 @@ +window.onload = function() { + // + + // the following lines will be replaced by docker/configurator, when it runs in a docker-container + window.ui = SwaggerUIBundle({ + //url: "https://petstore.swagger.io/v2/swagger.json", + url: "../swagger.json.php", + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout" + }); + + // +}; diff --git a/plugin/API/docs/swagger-ui-bundle.js b/plugin/API/docs/swagger-ui-bundle.js new file mode 100644 index 0000000000..edda4f6b68 --- /dev/null +++ b/plugin/API/docs/swagger-ui-bundle.js @@ -0,0 +1,2 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={251:(s,o)=>{o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<>1,L=-7,B=i?u-1:0,$=i?-1:1,V=s[o+B];for(B+=$,_=V&(1<<-L)-1,V>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(V?-1:1);w+=Math.pow(2,a),_-=j}return(V?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,V=a?0:_-1,U=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+V]=255&x,V+=U,x/=256,u-=8);for(w=w<0;s[i+V]=255&w,V+=U,w/=256,j-=8);s[i+V-U]|=128*z}},462:(s,o,i)=>{"use strict";var a=i(40975);s.exports=a},659:(s,o,i)=>{var a=i(51873),u=Object.prototype,_=u.hasOwnProperty,w=u.toString,x=a?a.toStringTag:void 0;s.exports=function getRawTag(s){var o=_.call(s,x),i=s[x];try{s[x]=void 0;var a=!0}catch(s){}var u=w.call(s);return a&&(o?s[x]=i:delete s[x]),u}},694:(s,o,i)=>{"use strict";i(91599);var a=i(37257);i(12560),s.exports=a},953:(s,o,i)=>{"use strict";s.exports=i(53375)},1733:s=>{var o=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;s.exports=function asciiWords(s){return s.match(o)||[]}},1882:(s,o,i)=>{var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},1907:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype,_=u.call,w=a&&u.bind.bind(_,_);s.exports=a?w:function(s){return function(){return _.apply(s,arguments)}}},2205:function(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},2209:(s,o,i)=>{"use strict";var a,u=i(9404),_=function productionTypeChecker(){invariant(!1,"ImmutablePropTypes type checking code is stripped in production.")};_.isRequired=_;var w=function getProductionTypeChecker(){return _};function getPropType(s){var o=typeof s;return Array.isArray(s)?"array":s instanceof RegExp?"object":s instanceof u.Iterable?"Immutable."+s.toSource().split(" ")[0]:o}function createChainableTypeChecker(s){function checkType(o,i,a,u,_,w){for(var x=arguments.length,C=Array(x>6?x-6:0),j=6;j>",null!=i[a]?s.apply(void 0,[i,a,u,_,w].concat(C)):o?new Error("Required "+_+" `"+w+"` was not specified in `"+u+"`."):void 0}var o=checkType.bind(null,!1);return o.isRequired=checkType.bind(null,!0),o}function createIterableSubclassTypeChecker(s,o){return function createImmutableTypeChecker(s,o){return createChainableTypeChecker((function validate(i,a,u,_,w){var x=i[a];if(!o(x)){var C=getPropType(x);return new Error("Invalid "+_+" `"+w+"` of type `"+C+"` supplied to `"+u+"`, expected `"+s+"`.")}return null}))}("Iterable."+s,(function(s){return u.Iterable.isIterable(s)&&o(s)}))}(a={listOf:w,mapOf:w,orderedMapOf:w,setOf:w,orderedSetOf:w,stackOf:w,iterableOf:w,recordOf:w,shape:w,contains:w,mapContains:w,orderedMapContains:w,list:_,map:_,orderedMap:_,set:_,orderedSet:_,stack:_,seq:_,record:_,iterable:_}).iterable.indexed=createIterableSubclassTypeChecker("Indexed",u.Iterable.isIndexed),a.iterable.keyed=createIterableSubclassTypeChecker("Keyed",u.Iterable.isKeyed),s.exports=a},2404:(s,o,i)=>{var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},2523:s=>{s.exports=function baseFindIndex(s,o,i,a){for(var u=s.length,_=i+(a?1:-1);a?_--:++_{"use strict";var a=i(45951),u=Object.defineProperty;s.exports=function(s,o){try{u(a,s,{value:o,configurable:!0,writable:!0})}catch(i){a[s]=o}return o}},2694:(s,o,i)=>{"use strict";var a=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,s.exports=function(){function shim(s,o,i,u,_,w){if(w!==a){var x=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw x.name="Invariant Violation",x}}function getShim(){return shim}shim.isRequired=shim;var s={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return s.PropTypes=s,s}},2874:s=>{s.exports={}},2875:(s,o,i)=>{"use strict";var a=i(23045),u=i(80376);s.exports=Object.keys||function keys(s){return a(s,u)}},2955:(s,o,i)=>{"use strict";var a,u=i(65606);function _defineProperty(s,o,i){return(o=function _toPropertyKey(s){var o=function _toPrimitive(s,o){if("object"!=typeof s||null===s)return s;var i=s[Symbol.toPrimitive];if(void 0!==i){var a=i.call(s,o||"default");if("object"!=typeof a)return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===o?String:Number)(s)}(s,"string");return"symbol"==typeof o?o:String(o)}(o))in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}var _=i(86238),w=Symbol("lastResolve"),x=Symbol("lastReject"),C=Symbol("error"),j=Symbol("ended"),L=Symbol("lastPromise"),B=Symbol("handlePromise"),$=Symbol("stream");function createIterResult(s,o){return{value:s,done:o}}function readAndResolve(s){var o=s[w];if(null!==o){var i=s[$].read();null!==i&&(s[L]=null,s[w]=null,s[x]=null,o(createIterResult(i,!1)))}}function onReadable(s){u.nextTick(readAndResolve,s)}var V=Object.getPrototypeOf((function(){})),U=Object.setPrototypeOf((_defineProperty(a={get stream(){return this[$]},next:function next(){var s=this,o=this[C];if(null!==o)return Promise.reject(o);if(this[j])return Promise.resolve(createIterResult(void 0,!0));if(this[$].destroyed)return new Promise((function(o,i){u.nextTick((function(){s[C]?i(s[C]):o(createIterResult(void 0,!0))}))}));var i,a=this[L];if(a)i=new Promise(function wrapForNext(s,o){return function(i,a){s.then((function(){o[j]?i(createIterResult(void 0,!0)):o[B](i,a)}),a)}}(a,this));else{var _=this[$].read();if(null!==_)return Promise.resolve(createIterResult(_,!1));i=new Promise(this[B])}return this[L]=i,i}},Symbol.asyncIterator,(function(){return this})),_defineProperty(a,"return",(function _return(){var s=this;return new Promise((function(o,i){s[$].destroy(null,(function(s){s?i(s):o(createIterResult(void 0,!0))}))}))})),a),V);s.exports=function createReadableStreamAsyncIterator(s){var o,i=Object.create(U,(_defineProperty(o={},$,{value:s,writable:!0}),_defineProperty(o,w,{value:null,writable:!0}),_defineProperty(o,x,{value:null,writable:!0}),_defineProperty(o,C,{value:null,writable:!0}),_defineProperty(o,j,{value:s._readableState.endEmitted,writable:!0}),_defineProperty(o,B,{value:function value(s,o){var a=i[$].read();a?(i[L]=null,i[w]=null,i[x]=null,s(createIterResult(a,!1))):(i[w]=s,i[x]=o)},writable:!0}),o));return i[L]=null,_(s,(function(s){if(s&&"ERR_STREAM_PREMATURE_CLOSE"!==s.code){var o=i[x];return null!==o&&(i[L]=null,i[w]=null,i[x]=null,o(s)),void(i[C]=s)}var a=i[w];null!==a&&(i[L]=null,i[w]=null,i[x]=null,a(createIterResult(void 0,!0))),i[j]=!0})),s.on("readable",onReadable.bind(null,i)),i}},3110:(s,o,i)=>{const a=i(5187),u=i(85015),_=i(98023),w=i(53812),x=i(23805),C=i(85105),j=i(86804);class Namespace{constructor(s){this.elementMap={},this.elementDetection=[],this.Element=j.Element,this.KeyValuePair=j.KeyValuePair,s&&s.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(s){return s.namespace&&s.namespace({base:this}),s.load&&s.load({base:this}),this}useDefault(){return this.register("null",j.NullElement).register("string",j.StringElement).register("number",j.NumberElement).register("boolean",j.BooleanElement).register("array",j.ArrayElement).register("object",j.ObjectElement).register("member",j.MemberElement).register("ref",j.RefElement).register("link",j.LinkElement),this.detect(a,j.NullElement,!1).detect(u,j.StringElement,!1).detect(_,j.NumberElement,!1).detect(w,j.BooleanElement,!1).detect(Array.isArray,j.ArrayElement,!1).detect(x,j.ObjectElement,!1),this}register(s,o){return this._elements=void 0,this.elementMap[s]=o,this}unregister(s){return this._elements=void 0,delete this.elementMap[s],this}detect(s,o,i){return void 0===i||i?this.elementDetection.unshift([s,o]):this.elementDetection.push([s,o]),this}toElement(s){if(s instanceof this.Element)return s;let o;for(let i=0;i{const o=s[0].toUpperCase()+s.substr(1);this._elements[o]=this.elementMap[s]}))),this._elements}get serialiser(){return new C(this)}}C.prototype.Namespace=Namespace,s.exports=Namespace},3121:(s,o,i)=>{"use strict";var a=i(65482),u=Math.min;s.exports=function(s){var o=a(s);return o>0?u(o,9007199254740991):0}},3209:(s,o,i)=>{var a=i(91596),u=i(53320),_=i(36306),w="__lodash_placeholder__",x=128,C=Math.min;s.exports=function mergeData(s,o){var i=s[1],j=o[1],L=i|j,B=L<131,$=j==x&&8==i||j==x&&256==i&&s[7].length<=o[8]||384==j&&o[7].length<=o[8]&&8==i;if(!B&&!$)return s;1&j&&(s[2]=o[2],L|=1&i?0:4);var V=o[3];if(V){var U=s[3];s[3]=U?a(U,V,o[4]):V,s[4]=U?_(s[3],w):o[4]}return(V=o[5])&&(U=s[5],s[5]=U?u(U,V,o[6]):V,s[6]=U?_(s[5],w):o[6]),(V=o[7])&&(s[7]=V),j&x&&(s[8]=null==s[8]?o[8]:C(s[8],o[8])),null==s[9]&&(s[9]=o[9]),s[0]=o[0],s[1]=L,s}},3650:(s,o,i)=>{var a=i(74335)(Object.keys,Object);s.exports=a},3656:(s,o,i)=>{s=i.nmd(s);var a=i(9325),u=i(89935),_=o&&!o.nodeType&&o,w=_&&s&&!s.nodeType&&s,x=w&&w.exports===_?a.Buffer:void 0,C=(x?x.isBuffer:void 0)||u;s.exports=C},4509:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheHas(s){return a(this,s).has(s)}},4640:s=>{"use strict";var o=String;s.exports=function(s){try{return o(s)}catch(s){return"Object"}}},4664:(s,o,i)=>{var a=i(79770),u=i(63345),_=Object.prototype.propertyIsEnumerable,w=Object.getOwnPropertySymbols,x=w?function(s){return null==s?[]:(s=Object(s),a(w(s),(function(o){return _.call(s,o)})))}:u;s.exports=x},4901:(s,o,i)=>{var a=i(72552),u=i(30294),_=i(40346),w={};w["[object Float32Array]"]=w["[object Float64Array]"]=w["[object Int8Array]"]=w["[object Int16Array]"]=w["[object Int32Array]"]=w["[object Uint8Array]"]=w["[object Uint8ClampedArray]"]=w["[object Uint16Array]"]=w["[object Uint32Array]"]=!0,w["[object Arguments]"]=w["[object Array]"]=w["[object ArrayBuffer]"]=w["[object Boolean]"]=w["[object DataView]"]=w["[object Date]"]=w["[object Error]"]=w["[object Function]"]=w["[object Map]"]=w["[object Number]"]=w["[object Object]"]=w["[object RegExp]"]=w["[object Set]"]=w["[object String]"]=w["[object WeakMap]"]=!1,s.exports=function baseIsTypedArray(s){return _(s)&&u(s.length)&&!!w[a(s)]}},4993:(s,o,i)=>{"use strict";var a=i(16946),u=i(74239);s.exports=function(s){return a(u(s))}},5187:s=>{s.exports=function isNull(s){return null===s}},5419:s=>{s.exports=function(s,o,i,a){var u=new Blob(void 0!==a?[a,s]:[s],{type:i||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(u,o);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(u):window.webkitURL.createObjectURL(u),w=document.createElement("a");w.style.display="none",w.href=_,w.setAttribute("download",o),void 0===w.download&&w.setAttribute("target","_blank"),document.body.appendChild(w),w.click(),setTimeout((function(){document.body.removeChild(w),window.URL.revokeObjectURL(_)}),200)}}},5556:(s,o,i)=>{s.exports=i(2694)()},5861:(s,o,i)=>{var a=i(55580),u=i(68223),_=i(32804),w=i(76545),x=i(28303),C=i(72552),j=i(47473),L="[object Map]",B="[object Promise]",$="[object Set]",V="[object WeakMap]",U="[object DataView]",z=j(a),Y=j(u),Z=j(_),ee=j(w),ie=j(x),ae=C;(a&&ae(new a(new ArrayBuffer(1)))!=U||u&&ae(new u)!=L||_&&ae(_.resolve())!=B||w&&ae(new w)!=$||x&&ae(new x)!=V)&&(ae=function(s){var o=C(s),i="[object Object]"==o?s.constructor:void 0,a=i?j(i):"";if(a)switch(a){case z:return U;case Y:return L;case Z:return B;case ee:return $;case ie:return V}return o}),s.exports=ae},6048:s=>{s.exports=function negate(s){if("function"!=typeof s)throw new TypeError("Expected a function");return function(){var o=arguments;switch(o.length){case 0:return!s.call(this);case 1:return!s.call(this,o[0]);case 2:return!s.call(this,o[0],o[1]);case 3:return!s.call(this,o[0],o[1],o[2])}return!s.apply(this,o)}}},6205:s=>{s.exports={ROOT:0,GROUP:1,POSITION:2,SET:3,RANGE:4,REPETITION:5,REFERENCE:6,CHAR:7}},6233:(s,o,i)=>{const a=i(6048),u=i(10316),_=i(92340);class ArrayElement extends u{constructor(s,o,i){super(s||[],o,i),this.element="array"}primitive(){return"array"}get(s){return this.content[s]}getValue(s){const o=this.get(s);if(o)return o.toValue()}getIndex(s){return this.content[s]}set(s,o){return this.content[s]=this.refract(o),this}remove(s){const o=this.content.splice(s,1);return o.length?o[0]:null}map(s,o){return this.content.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return new _(this.content.filter(s,o))}reject(s,o){return this.filter(a(s),o)}reduce(s,o){let i,a;void 0!==o?(i=0,a=this.refract(o)):(i=1,a="object"===this.primitive()?this.first.value:this.first);for(let o=i;o{s.bind(o)(i,this.refract(a))}))}shift(){return this.content.shift()}unshift(s){this.content.unshift(this.refract(s))}push(s){return this.content.push(this.refract(s)),this}add(s){this.push(s)}findElements(s,o){const i=o||{},a=!!i.recursive,u=void 0===i.results?[]:i.results;return this.forEach(((o,i,_)=>{a&&void 0!==o.findElements&&o.findElements(s,{results:u,recursive:a}),s(o,i,_)&&u.push(o)})),u}find(s){return new _(this.findElements(s,{recursive:!0}))}findByElement(s){return this.find((o=>o.element===s))}findByClass(s){return this.find((o=>o.classes.includes(s)))}getById(s){return this.find((o=>o.id.toValue()===s)).first}includes(s){return this.content.some((o=>o.equals(s)))}contains(s){return this.includes(s)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(s){return new this.constructor(this.content.concat(s.content))}"fantasy-land/concat"(s){return this.concat(s)}"fantasy-land/map"(s){return new this.constructor(this.map(s))}"fantasy-land/chain"(s){return this.map((o=>s(o)),this).reduce(((s,o)=>s.concat(o)),this.empty())}"fantasy-land/filter"(s){return new this.constructor(this.content.filter(s))}"fantasy-land/reduce"(s,o){return this.content.reduce(s,o)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),s.exports=ArrayElement},6499:(s,o,i)=>{"use strict";var a=i(1907),u=0,_=Math.random(),w=a(1..toString);s.exports=function(s){return"Symbol("+(void 0===s?"":s)+")_"+w(++u+_,36)}},6925:s=>{"use strict";s.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},7057:(s,o,i)=>{"use strict";var a=i(11470).charAt,u=i(90160),_=i(64932),w=i(60183),x=i(59550),C="String Iterator",j=_.set,L=_.getterFor(C);w(String,"String",(function(s){j(this,{type:C,string:u(s),index:0})}),(function next(){var s,o=L(this),i=o.string,u=o.index;return u>=i.length?x(void 0,!0):(s=a(i,u),o.index+=s.length,x(s,!1))}))},7309:(s,o,i)=>{var a=i(62006)(i(24713));s.exports=a},7376:s=>{"use strict";s.exports=!0},7463:(s,o,i)=>{"use strict";var a=i(98828),u=i(62250),_=/#|\.prototype\./,isForced=function(s,o){var i=x[w(s)];return i===j||i!==C&&(u(o)?a(o):!!o)},w=isForced.normalize=function(s){return String(s).replace(_,".").toLowerCase()},x=isForced.data={},C=isForced.NATIVE="N",j=isForced.POLYFILL="P";s.exports=isForced},7666:(s,o,i)=>{var a=i(84851),u=i(953);function _extends(){var o;return s.exports=_extends=a?u(o=a).call(o):function(s){for(var o=1;o{const a=i(6205);o.wordBoundary=()=>({type:a.POSITION,value:"b"}),o.nonWordBoundary=()=>({type:a.POSITION,value:"B"}),o.begin=()=>({type:a.POSITION,value:"^"}),o.end=()=>({type:a.POSITION,value:"$"})},8068:s=>{"use strict";var o=(()=>{var s=Object.defineProperty,o=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getOwnPropertySymbols,u=Object.prototype.hasOwnProperty,_=Object.prototype.propertyIsEnumerable,__defNormalProp=(o,i,a)=>i in o?s(o,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):o[i]=a,__spreadValues=(s,o)=>{for(var i in o||(o={}))u.call(o,i)&&__defNormalProp(s,i,o[i]);if(a)for(var i of a(o))_.call(o,i)&&__defNormalProp(s,i,o[i]);return s},__publicField=(s,o,i)=>(__defNormalProp(s,"symbol"!=typeof o?o+"":o,i),i),w={};((o,i)=>{for(var a in i)s(o,a,{get:i[a],enumerable:!0})})(w,{DEFAULT_OPTIONS:()=>C,DEFAULT_UUID_LENGTH:()=>x,default:()=>B});var x=6,C={dictionary:"alphanum",shuffle:!0,debug:!1,length:x,counter:0},j=class _ShortUniqueId{constructor(s={}){__publicField(this,"counter"),__publicField(this,"debug"),__publicField(this,"dict"),__publicField(this,"version"),__publicField(this,"dictIndex",0),__publicField(this,"dictRange",[]),__publicField(this,"lowerBound",0),__publicField(this,"upperBound",0),__publicField(this,"dictLength",0),__publicField(this,"uuidLength"),__publicField(this,"_digit_first_ascii",48),__publicField(this,"_digit_last_ascii",58),__publicField(this,"_alpha_lower_first_ascii",97),__publicField(this,"_alpha_lower_last_ascii",123),__publicField(this,"_hex_last_ascii",103),__publicField(this,"_alpha_upper_first_ascii",65),__publicField(this,"_alpha_upper_last_ascii",91),__publicField(this,"_number_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii]}),__publicField(this,"_alpha_dict_ranges",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alpha_lower_dict_ranges",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,"_alpha_upper_dict_ranges",{upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alphanum_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_alphanum_lower_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,"_alphanum_upper_dict_ranges",{digits:[this._digit_first_ascii,this._digit_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,"_hex_dict_ranges",{decDigits:[this._digit_first_ascii,this._digit_last_ascii],alphaDigits:[this._alpha_lower_first_ascii,this._hex_last_ascii]}),__publicField(this,"_dict_ranges",{_number_dict_ranges:this._number_dict_ranges,_alpha_dict_ranges:this._alpha_dict_ranges,_alpha_lower_dict_ranges:this._alpha_lower_dict_ranges,_alpha_upper_dict_ranges:this._alpha_upper_dict_ranges,_alphanum_dict_ranges:this._alphanum_dict_ranges,_alphanum_lower_dict_ranges:this._alphanum_lower_dict_ranges,_alphanum_upper_dict_ranges:this._alphanum_upper_dict_ranges,_hex_dict_ranges:this._hex_dict_ranges}),__publicField(this,"log",((...s)=>{const o=[...s];if(o[0]=`[short-unique-id] ${s[0]}`,!0===this.debug&&"undefined"!=typeof console&&null!==console)return console.log(...o)})),__publicField(this,"_normalizeDictionary",((s,o)=>{let i;if(s&&Array.isArray(s)&&s.length>1)i=s;else{let o;i=[],this.dictIndex=o=0;const a=`_${s}_dict_ranges`,u=this._dict_ranges[a];Object.keys(u).forEach((s=>{const a=s;for(this.dictRange=u[a],this.lowerBound=this.dictRange[0],this.upperBound=this.dictRange[1],this.dictIndex=o=this.lowerBound;this.lowerBound<=this.upperBound?othis.upperBound;this.dictIndex=this.lowerBound<=this.upperBound?o+=1:o-=1)i.push(String.fromCharCode(this.dictIndex))}))}if(o){const s=.5;i=i.sort((()=>Math.random()-s))}return i})),__publicField(this,"setDictionary",((s,o)=>{this.dict=this._normalizeDictionary(s,o),this.dictLength=this.dict.length,this.setCounter(0)})),__publicField(this,"seq",(()=>this.sequentialUUID())),__publicField(this,"sequentialUUID",(()=>{let s,o,i="";s=this.counter;do{o=s%this.dictLength,s=Math.trunc(s/this.dictLength),i+=this.dict[o]}while(0!==s);return this.counter+=1,i})),__publicField(this,"rnd",((s=this.uuidLength||x)=>this.randomUUID(s))),__publicField(this,"randomUUID",((s=this.uuidLength||x)=>{let o,i,a;if(null==s||s<1)throw new Error("Invalid UUID Length Provided");for(o="",a=0;athis.formattedUUID(s,o))),__publicField(this,"formattedUUID",((s,o)=>{const i={$r:this.randomUUID,$s:this.sequentialUUID,$t:this.stamp};return s.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g,(s=>{const a=s.slice(0,2),u=parseInt(s.slice(2),10);return"$s"===a?i[a]().padStart(u,"0"):"$t"===a&&o?i[a](u,o):i[a](u)}))})),__publicField(this,"availableUUIDs",((s=this.uuidLength)=>parseFloat(Math.pow([...new Set(this.dict)].length,s).toFixed(0)))),__publicField(this,"approxMaxBeforeCollision",((s=this.availableUUIDs(this.uuidLength))=>parseFloat(Math.sqrt(Math.PI/2*s).toFixed(20)))),__publicField(this,"collisionProbability",((s=this.availableUUIDs(this.uuidLength),o=this.uuidLength)=>parseFloat((this.approxMaxBeforeCollision(s)/this.availableUUIDs(o)).toFixed(20)))),__publicField(this,"uniqueness",((s=this.availableUUIDs(this.uuidLength))=>{const o=parseFloat((1-this.approxMaxBeforeCollision(s)/s).toFixed(20));return o>1?1:o<0?0:o})),__publicField(this,"getVersion",(()=>this.version)),__publicField(this,"stamp",((s,o)=>{const i=Math.floor(+(o||new Date)/1e3).toString(16);if("number"==typeof s&&0===s)return i;if("number"!=typeof s||s<10)throw new Error(["Param finalLength must be a number greater than or equal to 10,","or 0 if you want the raw hexadecimal timestamp"].join("\n"));const a=s-9,u=Math.round(Math.random()*(a>15?15:a)),_=this.randomUUID(a);return`${_.substring(0,u)}${i}${_.substring(u)}${u.toString(16)}`})),__publicField(this,"parseStamp",((s,o)=>{if(o&&!/t0|t[1-9]\d{1,}/.test(o))throw new Error("Cannot extract date from a formated UUID with no timestamp in the format");const i=o?o.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g,(s=>{const o={$r:s=>[...Array(s)].map((()=>"r")).join(""),$s:s=>[...Array(s)].map((()=>"s")).join(""),$t:s=>[...Array(s)].map((()=>"t")).join("")},i=s.slice(0,2),a=parseInt(s.slice(2),10);return o[i](a)})).replace(/^(.*?)(t{8,})(.*)$/g,((o,i,a)=>s.substring(i.length,i.length+a.length))):s;if(8===i.length)return new Date(1e3*parseInt(i,16));if(i.length<10)throw new Error("Stamp length invalid");const a=parseInt(i.substring(i.length-1),16);return new Date(1e3*parseInt(i.substring(a,a+8),16))})),__publicField(this,"setCounter",(s=>{this.counter=s})),__publicField(this,"validate",((s,o)=>{const i=o?this._normalizeDictionary(o):this.dict;return s.split("").every((s=>i.includes(s)))}));const o=__spreadValues(__spreadValues({},C),s);this.counter=0,this.debug=!1,this.dict=[],this.version="5.2.0";const{dictionary:i,shuffle:a,length:u,counter:_}=o;return this.uuidLength=u,this.setDictionary(i,a),this.setCounter(_),this.debug=o.debug,this.log(this.dict),this.log(`Generator instantiated with Dictionary Size ${this.dictLength} and counter set to ${this.counter}`),this.log=this.log.bind(this),this.setDictionary=this.setDictionary.bind(this),this.setCounter=this.setCounter.bind(this),this.seq=this.seq.bind(this),this.sequentialUUID=this.sequentialUUID.bind(this),this.rnd=this.rnd.bind(this),this.randomUUID=this.randomUUID.bind(this),this.fmt=this.fmt.bind(this),this.formattedUUID=this.formattedUUID.bind(this),this.availableUUIDs=this.availableUUIDs.bind(this),this.approxMaxBeforeCollision=this.approxMaxBeforeCollision.bind(this),this.collisionProbability=this.collisionProbability.bind(this),this.uniqueness=this.uniqueness.bind(this),this.getVersion=this.getVersion.bind(this),this.stamp=this.stamp.bind(this),this.parseStamp=this.parseStamp.bind(this),this}};__publicField(j,"default",j);var L,B=j;return L=w,((a,_,w,x)=>{if(_&&"object"==typeof _||"function"==typeof _)for(let C of i(_))u.call(a,C)||C===w||s(a,C,{get:()=>_[C],enumerable:!(x=o(_,C))||x.enumerable});return a})(s({},"__esModule",{value:!0}),L)})();s.exports=o.default,"undefined"!=typeof window&&(o=o.default)},9325:(s,o,i)=>{var a=i(34840),u="object"==typeof self&&self&&self.Object===Object&&self,_=a||u||Function("return this")();s.exports=_},9404:function(s){s.exports=function(){"use strict";var s=Array.prototype.slice;function createClass(s,o){o&&(s.prototype=Object.create(o.prototype)),s.prototype.constructor=s}function Iterable(s){return isIterable(s)?s:Seq(s)}function KeyedIterable(s){return isKeyed(s)?s:KeyedSeq(s)}function IndexedIterable(s){return isIndexed(s)?s:IndexedSeq(s)}function SetIterable(s){return isIterable(s)&&!isAssociative(s)?s:SetSeq(s)}function isIterable(s){return!(!s||!s[o])}function isKeyed(s){return!(!s||!s[i])}function isIndexed(s){return!(!s||!s[a])}function isAssociative(s){return isKeyed(s)||isIndexed(s)}function isOrdered(s){return!(!s||!s[u])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var o="@@__IMMUTABLE_ITERABLE__@@",i="@@__IMMUTABLE_KEYED__@@",a="@@__IMMUTABLE_INDEXED__@@",u="@@__IMMUTABLE_ORDERED__@@",_="delete",w=5,x=1<>>0;if(""+i!==o||4294967295===i)return NaN;o=i}return o<0?ensureSize(s)+o:o}function returnTrue(){return!0}function wholeSlice(s,o,i){return(0===s||void 0!==i&&s<=-i)&&(void 0===o||void 0!==i&&o>=i)}function resolveBegin(s,o){return resolveIndex(s,o,0)}function resolveEnd(s,o){return resolveIndex(s,o,o)}function resolveIndex(s,o,i){return void 0===s?i:s<0?Math.max(0,o+s):void 0===o?s:Math.min(o,s)}var $=0,V=1,U=2,z="function"==typeof Symbol&&Symbol.iterator,Y="@@iterator",Z=z||Y;function Iterator(s){this.next=s}function iteratorValue(s,o,i,a){var u=0===s?o:1===s?i:[o,i];return a?a.value=u:a={value:u,done:!1},a}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(s){return!!getIteratorFn(s)}function isIterator(s){return s&&"function"==typeof s.next}function getIterator(s){var o=getIteratorFn(s);return o&&o.call(s)}function getIteratorFn(s){var o=s&&(z&&s[z]||s[Y]);if("function"==typeof o)return o}function isArrayLike(s){return s&&"number"==typeof s.length}function Seq(s){return null==s?emptySequence():isIterable(s)?s.toSeq():seqFromValue(s)}function KeyedSeq(s){return null==s?emptySequence().toKeyedSeq():isIterable(s)?isKeyed(s)?s.toSeq():s.fromEntrySeq():keyedSeqFromValue(s)}function IndexedSeq(s){return null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s.toIndexedSeq():indexedSeqFromValue(s)}function SetSeq(s){return(null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s:indexedSeqFromValue(s)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=$,Iterator.VALUES=V,Iterator.ENTRIES=U,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[Z]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!0)},Seq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!1)},IndexedSeq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var ee,ie,ae,ce="@@__IMMUTABLE_SEQ__@@";function ArraySeq(s){this._array=s,this.size=s.length}function ObjectSeq(s){var o=Object.keys(s);this._object=s,this._keys=o,this.size=o.length}function IterableSeq(s){this._iterable=s,this.size=s.length||s.size}function IteratorSeq(s){this._iterator=s,this._iteratorCache=[]}function isSeq(s){return!(!s||!s[ce])}function emptySequence(){return ee||(ee=new ArraySeq([]))}function keyedSeqFromValue(s){var o=Array.isArray(s)?new ArraySeq(s).fromEntrySeq():isIterator(s)?new IteratorSeq(s).fromEntrySeq():hasIterator(s)?new IterableSeq(s).fromEntrySeq():"object"==typeof s?new ObjectSeq(s):void 0;if(!o)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+s);return o}function indexedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(!o)throw new TypeError("Expected Array or iterable object of values: "+s);return o}function seqFromValue(s){var o=maybeIndexedSeqFromValue(s)||"object"==typeof s&&new ObjectSeq(s);if(!o)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+s);return o}function maybeIndexedSeqFromValue(s){return isArrayLike(s)?new ArraySeq(s):isIterator(s)?new IteratorSeq(s):hasIterator(s)?new IterableSeq(s):void 0}function seqIterate(s,o,i,a){var u=s._cache;if(u){for(var _=u.length-1,w=0;w<=_;w++){var x=u[i?_-w:w];if(!1===o(x[1],a?x[0]:w,s))return w+1}return w}return s.__iterateUncached(o,i)}function seqIterator(s,o,i,a){var u=s._cache;if(u){var _=u.length-1,w=0;return new Iterator((function(){var s=u[i?_-w:w];return w++>_?iteratorDone():iteratorValue(o,a?s[0]:w-1,s[1])}))}return s.__iteratorUncached(o,i)}function fromJS(s,o){return o?fromJSWith(o,s,"",{"":s}):fromJSDefault(s)}function fromJSWith(s,o,i,a){return Array.isArray(o)?s.call(a,i,IndexedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):isPlainObj(o)?s.call(a,i,KeyedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):o}function fromJSDefault(s){return Array.isArray(s)?IndexedSeq(s).map(fromJSDefault).toList():isPlainObj(s)?KeyedSeq(s).map(fromJSDefault).toMap():s}function isPlainObj(s){return s&&(s.constructor===Object||void 0===s.constructor)}function is(s,o){if(s===o||s!=s&&o!=o)return!0;if(!s||!o)return!1;if("function"==typeof s.valueOf&&"function"==typeof o.valueOf){if((s=s.valueOf())===(o=o.valueOf())||s!=s&&o!=o)return!0;if(!s||!o)return!1}return!("function"!=typeof s.equals||"function"!=typeof o.equals||!s.equals(o))}function deepEqual(s,o){if(s===o)return!0;if(!isIterable(o)||void 0!==s.size&&void 0!==o.size&&s.size!==o.size||void 0!==s.__hash&&void 0!==o.__hash&&s.__hash!==o.__hash||isKeyed(s)!==isKeyed(o)||isIndexed(s)!==isIndexed(o)||isOrdered(s)!==isOrdered(o))return!1;if(0===s.size&&0===o.size)return!0;var i=!isAssociative(s);if(isOrdered(s)){var a=s.entries();return o.every((function(s,o){var u=a.next().value;return u&&is(u[1],s)&&(i||is(u[0],o))}))&&a.next().done}var u=!1;if(void 0===s.size)if(void 0===o.size)"function"==typeof s.cacheResult&&s.cacheResult();else{u=!0;var _=s;s=o,o=_}var w=!0,x=o.__iterate((function(o,a){if(i?!s.has(o):u?!is(o,s.get(a,j)):!is(s.get(a,j),o))return w=!1,!1}));return w&&s.size===x}function Repeat(s,o){if(!(this instanceof Repeat))return new Repeat(s,o);if(this._value=s,this.size=void 0===o?1/0:Math.max(0,o),0===this.size){if(ie)return ie;ie=this}}function invariant(s,o){if(!s)throw new Error(o)}function Range(s,o,i){if(!(this instanceof Range))return new Range(s,o,i);if(invariant(0!==i,"Cannot step a Range by 0"),s=s||0,void 0===o&&(o=1/0),i=void 0===i?1:Math.abs(i),oa?iteratorDone():iteratorValue(s,u,i[o?a-u++:u++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(s,o){return void 0===o||this.has(s)?this._object[s]:o},ObjectSeq.prototype.has=function(s){return this._object.hasOwnProperty(s)},ObjectSeq.prototype.__iterate=function(s,o){for(var i=this._object,a=this._keys,u=a.length-1,_=0;_<=u;_++){var w=a[o?u-_:_];if(!1===s(i[w],w,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(s,o){var i=this._object,a=this._keys,u=a.length-1,_=0;return new Iterator((function(){var w=a[o?u-_:_];return _++>u?iteratorDone():iteratorValue(s,w,i[w])}))},ObjectSeq.prototype[u]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);var i=getIterator(this._iterable),a=0;if(isIterator(i))for(var u;!(u=i.next()).done&&!1!==s(u.value,a++,this););return a},IterableSeq.prototype.__iteratorUncached=function(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=getIterator(this._iterable);if(!isIterator(i))return new Iterator(iteratorDone);var a=0;return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);for(var i,a=this._iterator,u=this._iteratorCache,_=0;_=a.length){var o=i.next();if(o.done)return o;a[u]=o.value}return iteratorValue(s,u,a[u++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(s,o){return this.has(s)?this._value:o},Repeat.prototype.includes=function(s){return is(this._value,s)},Repeat.prototype.slice=function(s,o){var i=this.size;return wholeSlice(s,o,i)?this:new Repeat(this._value,resolveEnd(o,i)-resolveBegin(s,i))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(s){return is(this._value,s)?0:-1},Repeat.prototype.lastIndexOf=function(s){return is(this._value,s)?this.size:-1},Repeat.prototype.__iterate=function(s,o){for(var i=0;i=0&&o=0&&ii?iteratorDone():iteratorValue(s,_++,w)}))},Range.prototype.equals=function(s){return s instanceof Range?this._start===s._start&&this._end===s._end&&this._step===s._step:deepEqual(this,s)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var le="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(s,o){var i=65535&(s|=0),a=65535&(o|=0);return i*a+((s>>>16)*a+i*(o>>>16)<<16>>>0)|0};function smi(s){return s>>>1&1073741824|3221225471&s}function hash(s){if(!1===s||null==s)return 0;if("function"==typeof s.valueOf&&(!1===(s=s.valueOf())||null==s))return 0;if(!0===s)return 1;var o=typeof s;if("number"===o){if(s!=s||s===1/0)return 0;var i=0|s;for(i!==s&&(i^=4294967295*s);s>4294967295;)i^=s/=4294967295;return smi(i)}if("string"===o)return s.length>Se?cachedHashString(s):hashString(s);if("function"==typeof s.hashCode)return s.hashCode();if("object"===o)return hashJSObj(s);if("function"==typeof s.toString)return hashString(s.toString());throw new Error("Value type "+o+" cannot be hashed.")}function cachedHashString(s){var o=Pe[s];return void 0===o&&(o=hashString(s),xe===we&&(xe=0,Pe={}),xe++,Pe[s]=o),o}function hashString(s){for(var o=0,i=0;i0)switch(s.nodeType){case 1:return s.uniqueID;case 9:return s.documentElement&&s.documentElement.uniqueID}}var fe,ye="function"==typeof WeakMap;ye&&(fe=new WeakMap);var be=0,_e="__immutablehash__";"function"==typeof Symbol&&(_e=Symbol(_e));var Se=16,we=255,xe=0,Pe={};function assertNotInfinite(s){invariant(s!==1/0,"Cannot perform this action with an infinite size.")}function Map(s){return null==s?emptyMap():isMap(s)&&!isOrdered(s)?s:emptyMap().withMutations((function(o){var i=KeyedIterable(s);assertNotInfinite(i.size),i.forEach((function(s,i){return o.set(i,s)}))}))}function isMap(s){return!(!s||!s[Re])}createClass(Map,KeyedCollection),Map.of=function(){var o=s.call(arguments,0);return emptyMap().withMutations((function(s){for(var i=0;i=o.length)throw new Error("Missing value for key: "+o[i]);s.set(o[i],o[i+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(s,o){return this._root?this._root.get(0,void 0,s,o):o},Map.prototype.set=function(s,o){return updateMap(this,s,o)},Map.prototype.setIn=function(s,o){return this.updateIn(s,j,(function(){return o}))},Map.prototype.remove=function(s){return updateMap(this,s,j)},Map.prototype.deleteIn=function(s){return this.updateIn(s,(function(){return j}))},Map.prototype.update=function(s,o,i){return 1===arguments.length?s(this):this.updateIn([s],o,i)},Map.prototype.updateIn=function(s,o,i){i||(i=o,o=void 0);var a=updateInDeepMap(this,forceIterator(s),o,i);return a===j?void 0:a},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(o){return mergeIntoMapWith(this,o,s.call(arguments,1))},Map.prototype.mergeIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.merge?s.merge.apply(s,i):i[i.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(o){var i=s.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(o),i)},Map.prototype.mergeDeepIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.mergeDeep?s.mergeDeep.apply(s,i):i[i.length-1]}))},Map.prototype.sort=function(s){return OrderedMap(sortFactory(this,s))},Map.prototype.sortBy=function(s,o){return OrderedMap(sortFactory(this,o,s))},Map.prototype.withMutations=function(s){var o=this.asMutable();return s(o),o.wasAltered()?o.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(s,o){return new MapIterator(this,s,o)},Map.prototype.__iterate=function(s,o){var i=this,a=0;return this._root&&this._root.iterate((function(o){return a++,s(o[1],o[0],i)}),o),a},Map.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeMap(this.size,this._root,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Map.isMap=isMap;var Te,Re="@@__IMMUTABLE_MAP__@@",qe=Map.prototype;function ArrayMapNode(s,o){this.ownerID=s,this.entries=o}function BitmapIndexedNode(s,o,i){this.ownerID=s,this.bitmap=o,this.nodes=i}function HashArrayMapNode(s,o,i){this.ownerID=s,this.count=o,this.nodes=i}function HashCollisionNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entries=i}function ValueNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entry=i}function MapIterator(s,o,i){this._type=o,this._reverse=i,this._stack=s._root&&mapIteratorFrame(s._root)}function mapIteratorValue(s,o){return iteratorValue(s,o[0],o[1])}function mapIteratorFrame(s,o){return{node:s,index:0,__prev:o}}function makeMap(s,o,i,a){var u=Object.create(qe);return u.size=s,u._root=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyMap(){return Te||(Te=makeMap(0))}function updateMap(s,o,i){var a,u;if(s._root){var _=MakeRef(L),w=MakeRef(B);if(a=updateNode(s._root,s.__ownerID,0,void 0,o,i,_,w),!w.value)return s;u=s.size+(_.value?i===j?-1:1:0)}else{if(i===j)return s;u=1,a=new ArrayMapNode(s.__ownerID,[[o,i]])}return s.__ownerID?(s.size=u,s._root=a,s.__hash=void 0,s.__altered=!0,s):a?makeMap(u,a):emptyMap()}function updateNode(s,o,i,a,u,_,w,x){return s?s.update(o,i,a,u,_,w,x):_===j?s:(SetRef(x),SetRef(w),new ValueNode(o,a,[u,_]))}function isLeafNode(s){return s.constructor===ValueNode||s.constructor===HashCollisionNode}function mergeIntoNode(s,o,i,a,u){if(s.keyHash===a)return new HashCollisionNode(o,a,[s.entry,u]);var _,x=(0===i?s.keyHash:s.keyHash>>>i)&C,j=(0===i?a:a>>>i)&C;return new BitmapIndexedNode(o,1<>>=1)w[C]=1&i?o[_++]:void 0;return w[a]=u,new HashArrayMapNode(s,_+1,w)}function mergeIntoMapWith(s,o,i){for(var a=[],u=0;u>1&1431655765))+(s>>2&858993459))+(s>>4)&252645135,s+=s>>8,127&(s+=s>>16)}function setIn(s,o,i,a){var u=a?s:arrCopy(s);return u[o]=i,u}function spliceIn(s,o,i,a){var u=s.length+1;if(a&&o+1===u)return s[o]=i,s;for(var _=new Array(u),w=0,x=0;x=$e)return createNodes(s,C,a,u);var V=s&&s===this.ownerID,U=V?C:arrCopy(C);return $?x?L===B-1?U.pop():U[L]=U.pop():U[L]=[a,u]:U.push([a,u]),V?(this.entries=U,this):new ArrayMapNode(s,U)}},BitmapIndexedNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=1<<((0===s?o:o>>>s)&C),_=this.bitmap;return _&u?this.nodes[popCount(_&u-1)].get(s+w,o,i,a):a},BitmapIndexedNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=1<=ze)return expandNodes(s,z,$,L,Z);if(V&&!Z&&2===z.length&&isLeafNode(z[1^U]))return z[1^U];if(V&&Z&&1===z.length&&isLeafNode(Z))return Z;var ee=s&&s===this.ownerID,ie=V?Z?$:$^B:$|B,ae=V?Z?setIn(z,U,Z,ee):spliceOut(z,U,ee):spliceIn(z,U,Z,ee);return ee?(this.bitmap=ie,this.nodes=ae,this):new BitmapIndexedNode(s,ie,ae)},HashArrayMapNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=(0===s?o:o>>>s)&C,_=this.nodes[u];return _?_.get(s+w,o,i,a):a},HashArrayMapNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=u===j,$=this.nodes,V=$[L];if(B&&!V)return this;var U=updateNode(V,s,o+w,i,a,u,_,x);if(U===V)return this;var z=this.count;if(V){if(!U&&--z0&&a=0&&s>>o&C;if(a>=this.array.length)return new VNode([],s);var u,_=0===a;if(o>0){var x=this.array[a];if((u=x&&x.removeBefore(s,o-w,i))===x&&_)return this}if(_&&!u)return this;var j=editableVNode(this,s);if(!_)for(var L=0;L>>o&C;if(u>=this.array.length)return this;if(o>0){var _=this.array[u];if((a=_&&_.removeAfter(s,o-w,i))===_&&u===this.array.length-1)return this}var x=editableVNode(this,s);return x.array.splice(u+1),a&&(x.array[u]=a),x};var Ye,Qe,et={};function iterateList(s,o){var i=s._origin,a=s._capacity,u=getTailOffset(a),_=s._tail;return iterateNodeOrLeaf(s._root,s._level,0);function iterateNodeOrLeaf(s,o,i){return 0===o?iterateLeaf(s,i):iterateNode(s,o,i)}function iterateLeaf(s,w){var C=w===u?_&&_.array:s&&s.array,j=w>i?0:i-w,L=a-w;return L>x&&(L=x),function(){if(j===L)return et;var s=o?--L:j++;return C&&C[s]}}function iterateNode(s,u,_){var C,j=s&&s.array,L=_>i?0:i-_>>u,B=1+(a-_>>u);return B>x&&(B=x),function(){for(;;){if(C){var s=C();if(s!==et)return s;C=null}if(L===B)return et;var i=o?--B:L++;C=iterateNodeOrLeaf(j&&j[i],u-w,_+(i<=s.size||o<0)return s.withMutations((function(s){o<0?setListBounds(s,o).set(0,i):setListBounds(s,0,o+1).set(o,i)}));o+=s._origin;var a=s._tail,u=s._root,_=MakeRef(B);return o>=getTailOffset(s._capacity)?a=updateVNode(a,s.__ownerID,0,o,i,_):u=updateVNode(u,s.__ownerID,s._level,o,i,_),_.value?s.__ownerID?(s._root=u,s._tail=a,s.__hash=void 0,s.__altered=!0,s):makeList(s._origin,s._capacity,s._level,u,a):s}function updateVNode(s,o,i,a,u,_){var x,j=a>>>i&C,L=s&&j0){var B=s&&s.array[j],$=updateVNode(B,o,i-w,a,u,_);return $===B?s:((x=editableVNode(s,o)).array[j]=$,x)}return L&&s.array[j]===u?s:(SetRef(_),x=editableVNode(s,o),void 0===u&&j===x.array.length-1?x.array.pop():x.array[j]=u,x)}function editableVNode(s,o){return o&&s&&o===s.ownerID?s:new VNode(s?s.array.slice():[],o)}function listNodeFor(s,o){if(o>=getTailOffset(s._capacity))return s._tail;if(o<1<0;)i=i.array[o>>>a&C],a-=w;return i}}function setListBounds(s,o,i){void 0!==o&&(o|=0),void 0!==i&&(i|=0);var a=s.__ownerID||new OwnerID,u=s._origin,_=s._capacity,x=u+o,j=void 0===i?_:i<0?_+i:u+i;if(x===u&&j===_)return s;if(x>=j)return s.clear();for(var L=s._level,B=s._root,$=0;x+$<0;)B=new VNode(B&&B.array.length?[void 0,B]:[],a),$+=1<<(L+=w);$&&(x+=$,u+=$,j+=$,_+=$);for(var V=getTailOffset(_),U=getTailOffset(j);U>=1<V?new VNode([],a):z;if(z&&U>V&&x<_&&z.array.length){for(var Z=B=editableVNode(B,a),ee=L;ee>w;ee-=w){var ie=V>>>ee&C;Z=Z.array[ie]=editableVNode(Z.array[ie],a)}Z.array[V>>>w&C]=z}if(j<_&&(Y=Y&&Y.removeAfter(a,0,j)),x>=U)x-=U,j-=U,L=w,B=null,Y=Y&&Y.removeBefore(a,0,x);else if(x>u||U>>L&C;if(ae!==U>>>L&C)break;ae&&($+=(1<u&&(B=B.removeBefore(a,L,x-$)),B&&Uu&&(u=x.size),isIterable(w)||(x=x.map((function(s){return fromJS(s)}))),a.push(x)}return u>s.size&&(s=s.setSize(u)),mergeIntoCollectionWith(s,o,a)}function getTailOffset(s){return s>>w<=x&&w.size>=2*_.size?(a=(u=w.filter((function(s,o){return void 0!==s&&C!==o}))).toKeyedSeq().map((function(s){return s[0]})).flip().toMap(),s.__ownerID&&(a.__ownerID=u.__ownerID=s.__ownerID)):(a=_.remove(o),u=C===w.size-1?w.pop():w.set(C,void 0))}else if(L){if(i===w.get(C)[1])return s;a=_,u=w.set(C,[o,i])}else a=_.set(o,w.size),u=w.set(w.size,[o,i]);return s.__ownerID?(s.size=a.size,s._map=a,s._list=u,s.__hash=void 0,s):makeOrderedMap(a,u)}function ToKeyedSequence(s,o){this._iter=s,this._useKeys=o,this.size=s.size}function ToIndexedSequence(s){this._iter=s,this.size=s.size}function ToSetSequence(s){this._iter=s,this.size=s.size}function FromEntriesSequence(s){this._iter=s,this.size=s.size}function flipFactory(s){var o=makeSequence(s);return o._iter=s,o.size=s.size,o.flip=function(){return s},o.reverse=function(){var o=s.reverse.apply(this);return o.flip=function(){return s.reverse()},o},o.has=function(o){return s.includes(o)},o.includes=function(o){return s.has(o)},o.cacheResult=cacheResultThrough,o.__iterateUncached=function(o,i){var a=this;return s.__iterate((function(s,i){return!1!==o(i,s,a)}),i)},o.__iteratorUncached=function(o,i){if(o===U){var a=s.__iterator(o,i);return new Iterator((function(){var s=a.next();if(!s.done){var o=s.value[0];s.value[0]=s.value[1],s.value[1]=o}return s}))}return s.__iterator(o===V?$:V,i)},o}function mapFactory(s,o,i){var a=makeSequence(s);return a.size=s.size,a.has=function(o){return s.has(o)},a.get=function(a,u){var _=s.get(a,j);return _===j?u:o.call(i,_,a,s)},a.__iterateUncached=function(a,u){var _=this;return s.__iterate((function(s,u,w){return!1!==a(o.call(i,s,u,w),u,_)}),u)},a.__iteratorUncached=function(a,u){var _=s.__iterator(U,u);return new Iterator((function(){var u=_.next();if(u.done)return u;var w=u.value,x=w[0];return iteratorValue(a,x,o.call(i,w[1],x,s),u)}))},a}function reverseFactory(s,o){var i=makeSequence(s);return i._iter=s,i.size=s.size,i.reverse=function(){return s},s.flip&&(i.flip=function(){var o=flipFactory(s);return o.reverse=function(){return s.flip()},o}),i.get=function(i,a){return s.get(o?i:-1-i,a)},i.has=function(i){return s.has(o?i:-1-i)},i.includes=function(o){return s.includes(o)},i.cacheResult=cacheResultThrough,i.__iterate=function(o,i){var a=this;return s.__iterate((function(s,i){return o(s,i,a)}),!i)},i.__iterator=function(o,i){return s.__iterator(o,!i)},i}function filterFactory(s,o,i,a){var u=makeSequence(s);return a&&(u.has=function(a){var u=s.get(a,j);return u!==j&&!!o.call(i,u,a,s)},u.get=function(a,u){var _=s.get(a,j);return _!==j&&o.call(i,_,a,s)?_:u}),u.__iterateUncached=function(u,_){var w=this,x=0;return s.__iterate((function(s,_,C){if(o.call(i,s,_,C))return x++,u(s,a?_:x-1,w)}),_),x},u.__iteratorUncached=function(u,_){var w=s.__iterator(U,_),x=0;return new Iterator((function(){for(;;){var _=w.next();if(_.done)return _;var C=_.value,j=C[0],L=C[1];if(o.call(i,L,j,s))return iteratorValue(u,a?j:x++,L,_)}}))},u}function countByFactory(s,o,i){var a=Map().asMutable();return s.__iterate((function(u,_){a.update(o.call(i,u,_,s),0,(function(s){return s+1}))})),a.asImmutable()}function groupByFactory(s,o,i){var a=isKeyed(s),u=(isOrdered(s)?OrderedMap():Map()).asMutable();s.__iterate((function(_,w){u.update(o.call(i,_,w,s),(function(s){return(s=s||[]).push(a?[w,_]:_),s}))}));var _=iterableClass(s);return u.map((function(o){return reify(s,_(o))}))}function sliceFactory(s,o,i,a){var u=s.size;if(void 0!==o&&(o|=0),void 0!==i&&(i===1/0?i=u:i|=0),wholeSlice(o,i,u))return s;var _=resolveBegin(o,u),w=resolveEnd(i,u);if(_!=_||w!=w)return sliceFactory(s.toSeq().cacheResult(),o,i,a);var x,C=w-_;C==C&&(x=C<0?0:C);var j=makeSequence(s);return j.size=0===x?x:s.size&&x||void 0,!a&&isSeq(s)&&x>=0&&(j.get=function(o,i){return(o=wrapIndex(this,o))>=0&&ox)return iteratorDone();var s=u.next();return a||o===V?s:iteratorValue(o,C-1,o===$?void 0:s.value[1],s)}))},j}function takeWhileFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterate(a,u);var w=0;return s.__iterate((function(s,u,x){return o.call(i,s,u,x)&&++w&&a(s,u,_)})),w},a.__iteratorUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterator(a,u);var w=s.__iterator(U,u),x=!0;return new Iterator((function(){if(!x)return iteratorDone();var s=w.next();if(s.done)return s;var u=s.value,C=u[0],j=u[1];return o.call(i,j,C,_)?a===U?s:iteratorValue(a,C,j,s):(x=!1,iteratorDone())}))},a}function skipWhileFactory(s,o,i,a){var u=makeSequence(s);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=!0,C=0;return s.__iterate((function(s,_,j){if(!x||!(x=o.call(i,s,_,j)))return C++,u(s,a?_:C-1,w)})),C},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=s.__iterator(U,_),C=!0,j=0;return new Iterator((function(){var s,_,L;do{if((s=x.next()).done)return a||u===V?s:iteratorValue(u,j++,u===$?void 0:s.value[1],s);var B=s.value;_=B[0],L=B[1],C&&(C=o.call(i,L,_,w))}while(C);return u===U?s:iteratorValue(u,_,L,s)}))},u}function concatFactory(s,o){var i=isKeyed(s),a=[s].concat(o).map((function(s){return isIterable(s)?i&&(s=KeyedIterable(s)):s=i?keyedSeqFromValue(s):indexedSeqFromValue(Array.isArray(s)?s:[s]),s})).filter((function(s){return 0!==s.size}));if(0===a.length)return s;if(1===a.length){var u=a[0];if(u===s||i&&isKeyed(u)||isIndexed(s)&&isIndexed(u))return u}var _=new ArraySeq(a);return i?_=_.toKeyedSeq():isIndexed(s)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=a.reduce((function(s,o){if(void 0!==s){var i=o.size;if(void 0!==i)return s+i}}),0),_}function flattenFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=0,w=!1;function flatDeep(s,x){var C=this;s.__iterate((function(s,u){return(!o||x0}function zipWithFactory(s,o,i){var a=makeSequence(s);return a.size=new ArraySeq(i).map((function(s){return s.size})).min(),a.__iterate=function(s,o){for(var i,a=this.__iterator(V,o),u=0;!(i=a.next()).done&&!1!==s(i.value,u++,this););return u},a.__iteratorUncached=function(s,a){var u=i.map((function(s){return s=Iterable(s),getIterator(a?s.reverse():s)})),_=0,w=!1;return new Iterator((function(){var i;return w||(i=u.map((function(s){return s.next()})),w=i.some((function(s){return s.done}))),w?iteratorDone():iteratorValue(s,_++,o.apply(null,i.map((function(s){return s.value}))))}))},a}function reify(s,o){return isSeq(s)?o:s.constructor(o)}function validateEntry(s){if(s!==Object(s))throw new TypeError("Expected [K, V] tuple: "+s)}function resolveSize(s){return assertNotInfinite(s.size),ensureSize(s)}function iterableClass(s){return isKeyed(s)?KeyedIterable:isIndexed(s)?IndexedIterable:SetIterable}function makeSequence(s){return Object.create((isKeyed(s)?KeyedSeq:isIndexed(s)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(s,o){return s>o?1:s=0;i--)o={value:arguments[i],next:o};return this.__ownerID?(this.size=s,this._head=o,this.__hash=void 0,this.__altered=!0,this):makeStack(s,o)},Stack.prototype.pushAll=function(s){if(0===(s=IndexedIterable(s)).size)return this;assertNotInfinite(s.size);var o=this.size,i=this._head;return s.reverse().forEach((function(s){o++,i={value:s,next:i}})),this.__ownerID?(this.size=o,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(o,i)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(s){return this.pushAll(s)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(s,o){if(wholeSlice(s,o,this.size))return this;var i=resolveBegin(s,this.size);if(resolveEnd(o,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,s,o);for(var a=this.size-i,u=this._head;i--;)u=u.next;return this.__ownerID?(this.size=a,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(a,u)},Stack.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeStack(this.size,this._head,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Stack.prototype.__iterate=function(s,o){if(o)return this.reverse().__iterate(s);for(var i=0,a=this._head;a&&!1!==s(a.value,i++,this);)a=a.next;return i},Stack.prototype.__iterator=function(s,o){if(o)return this.reverse().__iterator(s);var i=0,a=this._head;return new Iterator((function(){if(a){var o=a.value;return a=a.next,iteratorValue(s,i++,o)}return iteratorDone()}))},Stack.isStack=isStack;var at,ct="@@__IMMUTABLE_STACK__@@",lt=Stack.prototype;function makeStack(s,o,i,a){var u=Object.create(lt);return u.size=s,u._head=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyStack(){return at||(at=makeStack(0))}function mixin(s,o){var keyCopier=function(i){s.prototype[i]=o[i]};return Object.keys(o).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(o).forEach(keyCopier),s}lt[ct]=!0,lt.withMutations=qe.withMutations,lt.asMutable=qe.asMutable,lt.asImmutable=qe.asImmutable,lt.wasAltered=qe.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var s=new Array(this.size||0);return this.valueSeq().__iterate((function(o,i){s[i]=o})),s},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJS?s.toJS():s})).__toJS()},toJSON:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJSON?s.toJSON():s})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var s={};return this.__iterate((function(o,i){s[i]=o})),s},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(s,o){return 0===this.size?s+o:s+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+o},concat:function(){return reify(this,concatFactory(this,s.call(arguments,0)))},includes:function(s){return this.some((function(o){return is(o,s)}))},entries:function(){return this.__iterator(U)},every:function(s,o){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(a,u,_){if(!s.call(o,a,u,_))return i=!1,!1})),i},filter:function(s,o){return reify(this,filterFactory(this,s,o,!0))},find:function(s,o,i){var a=this.findEntry(s,o);return a?a[1]:i},forEach:function(s,o){return assertNotInfinite(this.size),this.__iterate(o?s.bind(o):s)},join:function(s){assertNotInfinite(this.size),s=void 0!==s?""+s:",";var o="",i=!0;return this.__iterate((function(a){i?i=!1:o+=s,o+=null!=a?a.toString():""})),o},keys:function(){return this.__iterator($)},map:function(s,o){return reify(this,mapFactory(this,s,o))},reduce:function(s,o,i){var a,u;return assertNotInfinite(this.size),arguments.length<2?u=!0:a=o,this.__iterate((function(o,_,w){u?(u=!1,a=o):a=s.call(i,a,o,_,w)})),a},reduceRight:function(s,o,i){var a=this.toKeyedSeq().reverse();return a.reduce.apply(a,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!0))},some:function(s,o){return!this.every(not(s),o)},sort:function(s){return reify(this,sortFactory(this,s))},values:function(){return this.__iterator(V)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(s,o){return ensureSize(s?this.toSeq().filter(s,o):this)},countBy:function(s,o){return countByFactory(this,s,o)},equals:function(s){return deepEqual(this,s)},entrySeq:function(){var s=this;if(s._cache)return new ArraySeq(s._cache);var o=s.toSeq().map(entryMapper).toIndexedSeq();return o.fromEntrySeq=function(){return s.toSeq()},o},filterNot:function(s,o){return this.filter(not(s),o)},findEntry:function(s,o,i){var a=i;return this.__iterate((function(i,u,_){if(s.call(o,i,u,_))return a=[u,i],!1})),a},findKey:function(s,o){var i=this.findEntry(s,o);return i&&i[0]},findLast:function(s,o,i){return this.toKeyedSeq().reverse().find(s,o,i)},findLastEntry:function(s,o,i){return this.toKeyedSeq().reverse().findEntry(s,o,i)},findLastKey:function(s,o){return this.toKeyedSeq().reverse().findKey(s,o)},first:function(){return this.find(returnTrue)},flatMap:function(s,o){return reify(this,flatMapFactory(this,s,o))},flatten:function(s){return reify(this,flattenFactory(this,s,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(s,o){return this.find((function(o,i){return is(i,s)}),void 0,o)},getIn:function(s,o){for(var i,a=this,u=forceIterator(s);!(i=u.next()).done;){var _=i.value;if((a=a&&a.get?a.get(_,j):j)===j)return o}return a},groupBy:function(s,o){return groupByFactory(this,s,o)},has:function(s){return this.get(s,j)!==j},hasIn:function(s){return this.getIn(s,j)!==j},isSubset:function(s){return s="function"==typeof s.includes?s:Iterable(s),this.every((function(o){return s.includes(o)}))},isSuperset:function(s){return(s="function"==typeof s.isSubset?s:Iterable(s)).isSubset(this)},keyOf:function(s){return this.findKey((function(o){return is(o,s)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(s){return this.toKeyedSeq().reverse().keyOf(s)},max:function(s){return maxFactory(this,s)},maxBy:function(s,o){return maxFactory(this,o,s)},min:function(s){return maxFactory(this,s?neg(s):defaultNegComparator)},minBy:function(s,o){return maxFactory(this,o?neg(o):defaultNegComparator,s)},rest:function(){return this.slice(1)},skip:function(s){return this.slice(Math.max(0,s))},skipLast:function(s){return reify(this,this.toSeq().reverse().skip(s).reverse())},skipWhile:function(s,o){return reify(this,skipWhileFactory(this,s,o,!0))},skipUntil:function(s,o){return this.skipWhile(not(s),o)},sortBy:function(s,o){return reify(this,sortFactory(this,o,s))},take:function(s){return this.slice(0,Math.max(0,s))},takeLast:function(s){return reify(this,this.toSeq().reverse().take(s).reverse())},takeWhile:function(s,o){return reify(this,takeWhileFactory(this,s,o))},takeUntil:function(s,o){return this.takeWhile(not(s),o)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var ut=Iterable.prototype;ut[o]=!0,ut[Z]=ut.values,ut.__toJS=ut.toArray,ut.__toStringMapper=quoteString,ut.inspect=ut.toSource=function(){return this.toString()},ut.chain=ut.flatMap,ut.contains=ut.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(s,o){var i=this,a=0;return reify(this,this.toSeq().map((function(u,_){return s.call(o,[_,u],a++,i)})).fromEntrySeq())},mapKeys:function(s,o){var i=this;return reify(this,this.toSeq().flip().map((function(a,u){return s.call(o,a,u,i)})).flip())}});var pt=KeyedIterable.prototype;function keyMapper(s,o){return o}function entryMapper(s,o){return[o,s]}function not(s){return function(){return!s.apply(this,arguments)}}function neg(s){return function(){return-s.apply(this,arguments)}}function quoteString(s){return"string"==typeof s?JSON.stringify(s):String(s)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(s,o){return so?-1:0}function hashIterable(s){if(s.size===1/0)return 0;var o=isOrdered(s),i=isKeyed(s),a=o?1:0;return murmurHashOfSize(s.__iterate(i?o?function(s,o){a=31*a+hashMerge(hash(s),hash(o))|0}:function(s,o){a=a+hashMerge(hash(s),hash(o))|0}:o?function(s){a=31*a+hash(s)|0}:function(s){a=a+hash(s)|0}),a)}function murmurHashOfSize(s,o){return o=le(o,3432918353),o=le(o<<15|o>>>-15,461845907),o=le(o<<13|o>>>-13,5),o=le((o=o+3864292196^s)^o>>>16,2246822507),o=smi((o=le(o^o>>>13,3266489909))^o>>>16)}function hashMerge(s,o){return s^o+2654435769+(s<<6)+(s>>2)}return pt[i]=!0,pt[Z]=ut.entries,pt.__toJS=ut.toObject,pt.__toStringMapper=function(s,o){return JSON.stringify(o)+": "+quoteString(s)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(s,o){return reify(this,filterFactory(this,s,o,!1))},findIndex:function(s,o){var i=this.findEntry(s,o);return i?i[0]:-1},indexOf:function(s){var o=this.keyOf(s);return void 0===o?-1:o},lastIndexOf:function(s){var o=this.lastKeyOf(s);return void 0===o?-1:o},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!1))},splice:function(s,o){var i=arguments.length;if(o=Math.max(0|o,0),0===i||2===i&&!o)return this;s=resolveBegin(s,s<0?this.count():this.size);var a=this.slice(0,s);return reify(this,1===i?a:a.concat(arrCopy(arguments,2),this.slice(s+o)))},findLastIndex:function(s,o){var i=this.findLastEntry(s,o);return i?i[0]:-1},first:function(){return this.get(0)},flatten:function(s){return reify(this,flattenFactory(this,s,!1))},get:function(s,o){return(s=wrapIndex(this,s))<0||this.size===1/0||void 0!==this.size&&s>this.size?o:this.find((function(o,i){return i===s}),void 0,o)},has:function(s){return(s=wrapIndex(this,s))>=0&&(void 0!==this.size?this.size===1/0||s{"use strict";i(71340);var a=i(92046);s.exports=a.Object.assign},9999:(s,o,i)=>{var a=i(37217),u=i(83729),_=i(16547),w=i(74733),x=i(43838),C=i(93290),j=i(23007),L=i(92271),B=i(48948),$=i(50002),V=i(83349),U=i(5861),z=i(76189),Y=i(77199),Z=i(35529),ee=i(56449),ie=i(3656),ae=i(87730),ce=i(23805),le=i(38440),pe=i(95950),de=i(37241),fe="[object Arguments]",ye="[object Function]",be="[object Object]",_e={};_e[fe]=_e["[object Array]"]=_e["[object ArrayBuffer]"]=_e["[object DataView]"]=_e["[object Boolean]"]=_e["[object Date]"]=_e["[object Float32Array]"]=_e["[object Float64Array]"]=_e["[object Int8Array]"]=_e["[object Int16Array]"]=_e["[object Int32Array]"]=_e["[object Map]"]=_e["[object Number]"]=_e[be]=_e["[object RegExp]"]=_e["[object Set]"]=_e["[object String]"]=_e["[object Symbol]"]=_e["[object Uint8Array]"]=_e["[object Uint8ClampedArray]"]=_e["[object Uint16Array]"]=_e["[object Uint32Array]"]=!0,_e["[object Error]"]=_e[ye]=_e["[object WeakMap]"]=!1,s.exports=function baseClone(s,o,i,Se,we,xe){var Pe,Te=1&o,Re=2&o,qe=4&o;if(i&&(Pe=we?i(s,Se,we,xe):i(s)),void 0!==Pe)return Pe;if(!ce(s))return s;var $e=ee(s);if($e){if(Pe=z(s),!Te)return j(s,Pe)}else{var ze=U(s),We=ze==ye||"[object GeneratorFunction]"==ze;if(ie(s))return C(s,Te);if(ze==be||ze==fe||We&&!we){if(Pe=Re||We?{}:Z(s),!Te)return Re?B(s,x(Pe,s)):L(s,w(Pe,s))}else{if(!_e[ze])return we?s:{};Pe=Y(s,ze,Te)}}xe||(xe=new a);var He=xe.get(s);if(He)return He;xe.set(s,Pe),le(s)?s.forEach((function(a){Pe.add(baseClone(a,o,i,a,s,xe))})):ae(s)&&s.forEach((function(a,u){Pe.set(u,baseClone(a,o,i,u,s,xe))}));var Xe=$e?void 0:(qe?Re?V:$:Re?de:pe)(s);return u(Xe||s,(function(a,u){Xe&&(a=s[u=a]),_(Pe,u,baseClone(a,o,i,u,s,xe))})),Pe}},10023:(s,o,i)=>{const a=i(6205),INTS=()=>[{type:a.RANGE,from:48,to:57}],WORDS=()=>[{type:a.CHAR,value:95},{type:a.RANGE,from:97,to:122},{type:a.RANGE,from:65,to:90}].concat(INTS()),WHITESPACE=()=>[{type:a.CHAR,value:9},{type:a.CHAR,value:10},{type:a.CHAR,value:11},{type:a.CHAR,value:12},{type:a.CHAR,value:13},{type:a.CHAR,value:32},{type:a.CHAR,value:160},{type:a.CHAR,value:5760},{type:a.RANGE,from:8192,to:8202},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233},{type:a.CHAR,value:8239},{type:a.CHAR,value:8287},{type:a.CHAR,value:12288},{type:a.CHAR,value:65279}];o.words=()=>({type:a.SET,set:WORDS(),not:!1}),o.notWords=()=>({type:a.SET,set:WORDS(),not:!0}),o.ints=()=>({type:a.SET,set:INTS(),not:!1}),o.notInts=()=>({type:a.SET,set:INTS(),not:!0}),o.whitespace=()=>({type:a.SET,set:WHITESPACE(),not:!1}),o.notWhitespace=()=>({type:a.SET,set:WHITESPACE(),not:!0}),o.anyChar=()=>({type:a.SET,set:[{type:a.CHAR,value:10},{type:a.CHAR,value:13},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233}],not:!0})},10043:(s,o,i)=>{"use strict";var a=i(54018),u=String,_=TypeError;s.exports=function(s){if(a(s))return s;throw new _("Can't set "+u(s)+" as a prototype")}},10124:(s,o,i)=>{var a=i(9325);s.exports=function(){return a.Date.now()}},10300:(s,o,i)=>{"use strict";var a=i(13930),u=i(82159),_=i(36624),w=i(4640),x=i(73448),C=TypeError;s.exports=function(s,o){var i=arguments.length<2?x(s):o;if(u(i))return _(a(i,s));throw new C(w(s)+" is not iterable")}},10316:(s,o,i)=>{const a=i(2404),u=i(55973),_=i(92340);class Element{constructor(s,o,i){o&&(this.meta=o),i&&(this.attributes=i),this.content=s}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((s=>{s.parent=this,s.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const s=new this.constructor;return s.element=this.element,this.meta.length&&(s._meta=this.meta.clone()),this.attributes.length&&(s._attributes=this.attributes.clone()),this.content?this.content.clone?s.content=this.content.clone():Array.isArray(this.content)?s.content=this.content.map((s=>s.clone())):s.content=this.content:s.content=this.content,s}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof u?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((s=>s.toValue()),this):this.content}toRef(s){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const o=new this.RefElement(this.id.toValue());return s&&(o.path=s),o}findRecursive(...s){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const o=s.pop();let i=new _;const append=(s,o)=>(s.push(o),s),checkElement=(s,i)=>{i.element===o&&s.push(i);const a=i.findRecursive(o);return a&&a.reduce(append,s),i.content instanceof u&&(i.content.key&&checkElement(s,i.content.key),i.content.value&&checkElement(s,i.content.value)),s};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),s.isEmpty||(i=i.filter((o=>{let i=o.parents.map((s=>s.element));for(const o in s){const a=s[o],u=i.indexOf(a);if(-1===u)return!1;i=i.splice(0,u)}return!0}))),i}set(s){return this.content=s,this}equals(s){return a(this.toValue(),s)}getMetaProperty(s,o){if(!this.meta.hasKey(s)){if(this.isFrozen){const s=this.refract(o);return s.freeze(),s}this.meta.set(s,o)}return this.meta.get(s)}setMetaProperty(s,o){this.meta.set(s,o)}get element(){return this._storedElement||"element"}set element(s){this._storedElement=s}get content(){return this._content}set content(s){if(s instanceof Element)this._content=s;else if(s instanceof _)this.content=s.elements;else if("string"==typeof s||"number"==typeof s||"boolean"==typeof s||"null"===s||null==s)this._content=s;else if(s instanceof u)this._content=s;else if(Array.isArray(s))this._content=s.map(this.refract);else{if("object"!=typeof s)throw new Error("Cannot set content to given value");this._content=Object.keys(s).map((o=>new this.MemberElement(o,s[o])))}}get meta(){if(!this._meta){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._meta=new this.ObjectElement}return this._meta}set meta(s){s instanceof this.ObjectElement?this._meta=s:this.meta.set(s||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._attributes=new this.ObjectElement}return this._attributes}set attributes(s){s instanceof this.ObjectElement?this._attributes=s:this.attributes.set(s||{})}get id(){return this.getMetaProperty("id","")}set id(s){this.setMetaProperty("id",s)}get classes(){return this.getMetaProperty("classes",[])}set classes(s){this.setMetaProperty("classes",s)}get title(){return this.getMetaProperty("title","")}set title(s){this.setMetaProperty("title",s)}get description(){return this.getMetaProperty("description","")}set description(s){this.setMetaProperty("description",s)}get links(){return this.getMetaProperty("links",[])}set links(s){this.setMetaProperty("links",s)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:s}=this;const o=new _;for(;s;)o.push(s),s=s.parent;return o}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof u){const s=new _([this.content.key]);return this.content.value&&s.push(this.content.value),s}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const s=new _;return this.children.forEach((o=>{s.push(o),o.recursiveChildren.forEach((o=>{s.push(o)}))})),s}}s.exports=Element},10392:s=>{s.exports=function getValue(s,o){return null==s?void 0:s[o]}},10776:(s,o,i)=>{var a=i(30756),u=i(95950);s.exports=function getMatchData(s){for(var o=u(s),i=o.length;i--;){var _=o[i],w=s[_];o[i]=[_,w,a(w)]}return o}},10866:(s,o,i)=>{const a=i(6048),u=i(92340);class ObjectSlice extends u{map(s,o){return this.elements.map((i=>s.bind(o)(i.value,i.key,i)))}filter(s,o){return new ObjectSlice(this.elements.filter((i=>s.bind(o)(i.value,i.key,i))))}reject(s,o){return this.filter(a(s.bind(o)))}forEach(s,o){return this.elements.forEach(((i,a)=>{s.bind(o)(i.value,i.key,i,a)}))}keys(){return this.map(((s,o)=>o.toValue()))}values(){return this.map((s=>s.toValue()))}}s.exports=ObjectSlice},11042:(s,o,i)=>{"use strict";var a=i(85582),u=i(1907),_=i(24443),w=i(87170),x=i(36624),C=u([].concat);s.exports=a("Reflect","ownKeys")||function ownKeys(s){var o=_.f(x(s)),i=w.f;return i?C(o,i(s)):o}},11091:(s,o,i)=>{"use strict";var a=i(45951),u=i(76024),_=i(92361),w=i(62250),x=i(13846).f,C=i(7463),j=i(92046),L=i(28311),B=i(61626),$=i(49724);i(36128);var wrapConstructor=function(s){var Wrapper=function(o,i,a){if(this instanceof Wrapper){switch(arguments.length){case 0:return new s;case 1:return new s(o);case 2:return new s(o,i)}return new s(o,i,a)}return u(s,this,arguments)};return Wrapper.prototype=s.prototype,Wrapper};s.exports=function(s,o){var i,u,V,U,z,Y,Z,ee,ie,ae=s.target,ce=s.global,le=s.stat,pe=s.proto,de=ce?a:le?a[ae]:a[ae]&&a[ae].prototype,fe=ce?j:j[ae]||B(j,ae,{})[ae],ye=fe.prototype;for(U in o)u=!(i=C(ce?U:ae+(le?".":"#")+U,s.forced))&&de&&$(de,U),Y=fe[U],u&&(Z=s.dontCallGetSet?(ie=x(de,U))&&ie.value:de[U]),z=u&&Z?Z:o[U],(i||pe||typeof Y!=typeof z)&&(ee=s.bind&&u?L(z,a):s.wrap&&u?wrapConstructor(z):pe&&w(z)?_(z):z,(s.sham||z&&z.sham||Y&&Y.sham)&&B(ee,"sham",!0),B(fe,U,ee),pe&&($(j,V=ae+"Prototype")||B(j,V,{}),B(j[V],U,z),s.real&&ye&&(i||!ye[U])&&B(ye,U,z)))}},11287:s=>{s.exports=function getHolder(s){return s.placeholder}},11331:(s,o,i)=>{var a=i(72552),u=i(28879),_=i(40346),w=Function.prototype,x=Object.prototype,C=w.toString,j=x.hasOwnProperty,L=C.call(Object);s.exports=function isPlainObject(s){if(!_(s)||"[object Object]"!=a(s))return!1;var o=u(s);if(null===o)return!0;var i=j.call(o,"constructor")&&o.constructor;return"function"==typeof i&&i instanceof i&&C.call(i)==L}},11470:(s,o,i)=>{"use strict";var a=i(1907),u=i(65482),_=i(90160),w=i(74239),x=a("".charAt),C=a("".charCodeAt),j=a("".slice),createMethod=function(s){return function(o,i){var a,L,B=_(w(o)),$=u(i),V=B.length;return $<0||$>=V?s?"":void 0:(a=C(B,$))<55296||a>56319||$+1===V||(L=C(B,$+1))<56320||L>57343?s?x(B,$):a:s?j(B,$,$+2):L-56320+(a-55296<<10)+65536}};s.exports={codeAt:createMethod(!1),charAt:createMethod(!0)}},11842:(s,o,i)=>{var a=i(82819),u=i(9325);s.exports=function createBind(s,o,i){var _=1&o,w=a(s);return function wrapper(){return(this&&this!==u&&this instanceof wrapper?w:s).apply(_?i:this,arguments)}}},12242:(s,o,i)=>{const a=i(10316);s.exports=class BooleanElement extends a{constructor(s,o,i){super(s,o,i),this.element="boolean"}primitive(){return"boolean"}}},12507:(s,o,i)=>{var a=i(28754),u=i(49698),_=i(63912),w=i(13222);s.exports=function createCaseFirst(s){return function(o){o=w(o);var i=u(o)?_(o):void 0,x=i?i[0]:o.charAt(0),C=i?a(i,1).join(""):o.slice(1);return x[s]()+C}}},12560:(s,o,i)=>{"use strict";i(99363);var a=i(19287),u=i(45951),_=i(14840),w=i(93742);for(var x in a)_(u[x],x),w[x]=w.Array},12651:(s,o,i)=>{var a=i(74218);s.exports=function getMapData(s,o){var i=s.__data__;return a(o)?i["string"==typeof o?"string":"hash"]:i.map}},12749:(s,o,i)=>{var a=i(81042),u=Object.prototype.hasOwnProperty;s.exports=function hashHas(s){var o=this.__data__;return a?void 0!==o[s]:u.call(o,s)}},13222:(s,o,i)=>{var a=i(77556);s.exports=function toString(s){return null==s?"":a(s)}},13846:(s,o,i)=>{"use strict";var a=i(39447),u=i(13930),_=i(22574),w=i(75817),x=i(4993),C=i(70470),j=i(49724),L=i(73648),B=Object.getOwnPropertyDescriptor;o.f=a?B:function getOwnPropertyDescriptor(s,o){if(s=x(s),o=C(o),L)try{return B(s,o)}catch(s){}if(j(s,o))return w(!u(_.f,s,o),s[o])}},13930:(s,o,i)=>{"use strict";var a=i(41505),u=Function.prototype.call;s.exports=a?u.bind(u):function(){return u.apply(u,arguments)}},14248:s=>{s.exports=function arraySome(s,o){for(var i=-1,a=null==s?0:s.length;++i{s.exports=function arrayPush(s,o){for(var i=-1,a=o.length,u=s.length;++i{const a=i(10316);s.exports=class RefElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(s){this.attributes.set("path",s)}}},14744:s=>{"use strict";var o=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&"object"==typeof s}(s)&&!function isSpecial(s){var o=Object.prototype.toString.call(s);return"[object RegExp]"===o||"[object Date]"===o||function isReactElement(s){return s.$$typeof===i}(s)}(s)};var i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(s,o){return!1!==o.clone&&o.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,o):s}function defaultArrayMerge(s,o,i){return s.concat(o).map((function(s){return cloneUnlessOtherwiseSpecified(s,i)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(o){return Object.propertyIsEnumerable.call(s,o)})):[]}(s))}function propertyIsOnObject(s,o){try{return o in s}catch(s){return!1}}function mergeObject(s,o,i){var a={};return i.isMergeableObject(s)&&getKeys(s).forEach((function(o){a[o]=cloneUnlessOtherwiseSpecified(s[o],i)})),getKeys(o).forEach((function(u){(function propertyIsUnsafe(s,o){return propertyIsOnObject(s,o)&&!(Object.hasOwnProperty.call(s,o)&&Object.propertyIsEnumerable.call(s,o))})(s,u)||(propertyIsOnObject(s,u)&&i.isMergeableObject(o[u])?a[u]=function getMergeFunction(s,o){if(!o.customMerge)return deepmerge;var i=o.customMerge(s);return"function"==typeof i?i:deepmerge}(u,i)(s[u],o[u],i):a[u]=cloneUnlessOtherwiseSpecified(o[u],i))})),a}function deepmerge(s,i,a){(a=a||{}).arrayMerge=a.arrayMerge||defaultArrayMerge,a.isMergeableObject=a.isMergeableObject||o,a.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var u=Array.isArray(i);return u===Array.isArray(s)?u?a.arrayMerge(s,i,a):mergeObject(s,i,a):cloneUnlessOtherwiseSpecified(i,a)}deepmerge.all=function deepmergeAll(s,o){if(!Array.isArray(s))throw new Error("first argument should be an array");return s.reduce((function(s,i){return deepmerge(s,i,o)}),{})};var a=deepmerge;s.exports=a},14792:(s,o,i)=>{var a=i(13222),u=i(55808);s.exports=function capitalize(s){return u(a(s).toLowerCase())}},14840:(s,o,i)=>{"use strict";var a=i(52623),u=i(74284).f,_=i(61626),w=i(49724),x=i(54878),C=i(76264)("toStringTag");s.exports=function(s,o,i,j){var L=i?s:s&&s.prototype;L&&(w(L,C)||u(L,C,{configurable:!0,value:o}),j&&!a&&_(L,"toString",x))}},14974:s=>{s.exports=function safeGet(s,o){if(("constructor"!==o||"function"!=typeof s[o])&&"__proto__"!=o)return s[o]}},15287:(s,o)=>{"use strict";var i=Symbol.for("react.element"),a=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),_=Symbol.for("react.strict_mode"),w=Symbol.for("react.profiler"),x=Symbol.for("react.provider"),C=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),L=Symbol.for("react.suspense"),B=Symbol.for("react.memo"),$=Symbol.for("react.lazy"),V=Symbol.iterator;var U={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},z=Object.assign,Y={};function E(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||U}function F(){}function G(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||U}E.prototype.isReactComponent={},E.prototype.setState=function(s,o){if("object"!=typeof s&&"function"!=typeof s&&null!=s)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,s,o,"setState")},E.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,"forceUpdate")},F.prototype=E.prototype;var Z=G.prototype=new F;Z.constructor=G,z(Z,E.prototype),Z.isPureReactComponent=!0;var ee=Array.isArray,ie=Object.prototype.hasOwnProperty,ae={current:null},ce={key:!0,ref:!0,__self:!0,__source:!0};function M(s,o,a){var u,_={},w=null,x=null;if(null!=o)for(u in void 0!==o.ref&&(x=o.ref),void 0!==o.key&&(w=""+o.key),o)ie.call(o,u)&&!ce.hasOwnProperty(u)&&(_[u]=o[u]);var C=arguments.length-2;if(1===C)_.children=a;else if(1{var a=i(96131);s.exports=function arrayIncludes(s,o){return!!(null==s?0:s.length)&&a(s,o,0)>-1}},15340:()=>{},15389:(s,o,i)=>{var a=i(93663),u=i(87978),_=i(83488),w=i(56449),x=i(50583);s.exports=function baseIteratee(s){return"function"==typeof s?s:null==s?_:"object"==typeof s?w(s)?u(s[0],s[1]):a(s):x(s)}},15972:(s,o,i)=>{"use strict";var a=i(49724),u=i(62250),_=i(39298),w=i(92522),x=i(57382),C=w("IE_PROTO"),j=Object,L=j.prototype;s.exports=x?j.getPrototypeOf:function(s){var o=_(s);if(a(o,C))return o[C];var i=o.constructor;return u(i)&&o instanceof i?i.prototype:o instanceof j?L:null}},16038:(s,o,i)=>{var a=i(5861),u=i(40346);s.exports=function baseIsSet(s){return u(s)&&"[object Set]"==a(s)}},16426:s=>{s.exports=function(){var s=document.getSelection();if(!s.rangeCount)return function(){};for(var o=document.activeElement,i=[],a=0;a{var a=i(43360),u=i(75288),_=Object.prototype.hasOwnProperty;s.exports=function assignValue(s,o,i){var w=s[o];_.call(s,o)&&u(w,i)&&(void 0!==i||o in s)||a(s,o,i)}},16708:(s,o,i)=>{"use strict";var a,u=i(65606);function CorkedRequest(s){var o=this;this.next=null,this.entry=null,this.finish=function(){!function onCorkedFinish(s,o,i){var a=s.entry;s.entry=null;for(;a;){var u=a.callback;o.pendingcb--,u(i),a=a.next}o.corkedRequestsFree.next=s}(o,s)}}s.exports=Writable,Writable.WritableState=WritableState;var _={deprecate:i(94643)},w=i(40345),x=i(48287).Buffer,C=(void 0!==i.g?i.g:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{}).Uint8Array||function(){};var j,L=i(75896),B=i(65291).getHighWaterMark,$=i(86048).F,V=$.ERR_INVALID_ARG_TYPE,U=$.ERR_METHOD_NOT_IMPLEMENTED,z=$.ERR_MULTIPLE_CALLBACK,Y=$.ERR_STREAM_CANNOT_PIPE,Z=$.ERR_STREAM_DESTROYED,ee=$.ERR_STREAM_NULL_VALUES,ie=$.ERR_STREAM_WRITE_AFTER_END,ae=$.ERR_UNKNOWN_ENCODING,ce=L.errorOrDestroy;function nop(){}function WritableState(s,o,_){a=a||i(25382),s=s||{},"boolean"!=typeof _&&(_=o instanceof a),this.objectMode=!!s.objectMode,_&&(this.objectMode=this.objectMode||!!s.writableObjectMode),this.highWaterMark=B(this,s,"writableHighWaterMark",_),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var w=!1===s.decodeStrings;this.decodeStrings=!w,this.defaultEncoding=s.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(s){!function onwrite(s,o){var i=s._writableState,a=i.sync,_=i.writecb;if("function"!=typeof _)throw new z;if(function onwriteStateUpdate(s){s.writing=!1,s.writecb=null,s.length-=s.writelen,s.writelen=0}(i),o)!function onwriteError(s,o,i,a,_){--o.pendingcb,i?(u.nextTick(_,a),u.nextTick(finishMaybe,s,o),s._writableState.errorEmitted=!0,ce(s,a)):(_(a),s._writableState.errorEmitted=!0,ce(s,a),finishMaybe(s,o))}(s,i,a,o,_);else{var w=needFinish(i)||s.destroyed;w||i.corked||i.bufferProcessing||!i.bufferedRequest||clearBuffer(s,i),a?u.nextTick(afterWrite,s,i,w,_):afterWrite(s,i,w,_)}}(o,s)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=!1!==s.emitClose,this.autoDestroy=!!s.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new CorkedRequest(this)}function Writable(s){var o=this instanceof(a=a||i(25382));if(!o&&!j.call(Writable,this))return new Writable(s);this._writableState=new WritableState(s,this,o),this.writable=!0,s&&("function"==typeof s.write&&(this._write=s.write),"function"==typeof s.writev&&(this._writev=s.writev),"function"==typeof s.destroy&&(this._destroy=s.destroy),"function"==typeof s.final&&(this._final=s.final)),w.call(this)}function doWrite(s,o,i,a,u,_,w){o.writelen=a,o.writecb=w,o.writing=!0,o.sync=!0,o.destroyed?o.onwrite(new Z("write")):i?s._writev(u,o.onwrite):s._write(u,_,o.onwrite),o.sync=!1}function afterWrite(s,o,i,a){i||function onwriteDrain(s,o){0===o.length&&o.needDrain&&(o.needDrain=!1,s.emit("drain"))}(s,o),o.pendingcb--,a(),finishMaybe(s,o)}function clearBuffer(s,o){o.bufferProcessing=!0;var i=o.bufferedRequest;if(s._writev&&i&&i.next){var a=o.bufferedRequestCount,u=new Array(a),_=o.corkedRequestsFree;_.entry=i;for(var w=0,x=!0;i;)u[w]=i,i.isBuf||(x=!1),i=i.next,w+=1;u.allBuffers=x,doWrite(s,o,!0,o.length,u,"",_.finish),o.pendingcb++,o.lastBufferedRequest=null,_.next?(o.corkedRequestsFree=_.next,_.next=null):o.corkedRequestsFree=new CorkedRequest(o),o.bufferedRequestCount=0}else{for(;i;){var C=i.chunk,j=i.encoding,L=i.callback;if(doWrite(s,o,!1,o.objectMode?1:C.length,C,j,L),i=i.next,o.bufferedRequestCount--,o.writing)break}null===i&&(o.lastBufferedRequest=null)}o.bufferedRequest=i,o.bufferProcessing=!1}function needFinish(s){return s.ending&&0===s.length&&null===s.bufferedRequest&&!s.finished&&!s.writing}function callFinal(s,o){s._final((function(i){o.pendingcb--,i&&ce(s,i),o.prefinished=!0,s.emit("prefinish"),finishMaybe(s,o)}))}function finishMaybe(s,o){var i=needFinish(o);if(i&&(function prefinish(s,o){o.prefinished||o.finalCalled||("function"!=typeof s._final||o.destroyed?(o.prefinished=!0,s.emit("prefinish")):(o.pendingcb++,o.finalCalled=!0,u.nextTick(callFinal,s,o)))}(s,o),0===o.pendingcb&&(o.finished=!0,s.emit("finish"),o.autoDestroy))){var a=s._readableState;(!a||a.autoDestroy&&a.endEmitted)&&s.destroy()}return i}i(56698)(Writable,w),WritableState.prototype.getBuffer=function getBuffer(){for(var s=this.bufferedRequest,o=[];s;)o.push(s),s=s.next;return o},function(){try{Object.defineProperty(WritableState.prototype,"buffer",{get:_.deprecate((function writableStateBufferGetter(){return this.getBuffer()}),"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(s){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(j=Function.prototype[Symbol.hasInstance],Object.defineProperty(Writable,Symbol.hasInstance,{value:function value(s){return!!j.call(this,s)||this===Writable&&(s&&s._writableState instanceof WritableState)}})):j=function realHasInstance(s){return s instanceof this},Writable.prototype.pipe=function(){ce(this,new Y)},Writable.prototype.write=function(s,o,i){var a=this._writableState,_=!1,w=!a.objectMode&&function _isUint8Array(s){return x.isBuffer(s)||s instanceof C}(s);return w&&!x.isBuffer(s)&&(s=function _uint8ArrayToBuffer(s){return x.from(s)}(s)),"function"==typeof o&&(i=o,o=null),w?o="buffer":o||(o=a.defaultEncoding),"function"!=typeof i&&(i=nop),a.ending?function writeAfterEnd(s,o){var i=new ie;ce(s,i),u.nextTick(o,i)}(this,i):(w||function validChunk(s,o,i,a){var _;return null===i?_=new ee:"string"==typeof i||o.objectMode||(_=new V("chunk",["string","Buffer"],i)),!_||(ce(s,_),u.nextTick(a,_),!1)}(this,a,s,i))&&(a.pendingcb++,_=function writeOrBuffer(s,o,i,a,u,_){if(!i){var w=function decodeChunk(s,o,i){s.objectMode||!1===s.decodeStrings||"string"!=typeof o||(o=x.from(o,i));return o}(o,a,u);a!==w&&(i=!0,u="buffer",a=w)}var C=o.objectMode?1:a.length;o.length+=C;var j=o.length-1))throw new ae(s);return this._writableState.defaultEncoding=s,this},Object.defineProperty(Writable.prototype,"writableBuffer",{enumerable:!1,get:function get(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(Writable.prototype,"writableHighWaterMark",{enumerable:!1,get:function get(){return this._writableState.highWaterMark}}),Writable.prototype._write=function(s,o,i){i(new U("_write()"))},Writable.prototype._writev=null,Writable.prototype.end=function(s,o,i){var a=this._writableState;return"function"==typeof s?(i=s,s=null,o=null):"function"==typeof o&&(i=o,o=null),null!=s&&this.write(s,o),a.corked&&(a.corked=1,this.uncork()),a.ending||function endWritable(s,o,i){o.ending=!0,finishMaybe(s,o),i&&(o.finished?u.nextTick(i):s.once("finish",i));o.ended=!0,s.writable=!1}(this,a,i),this},Object.defineProperty(Writable.prototype,"writableLength",{enumerable:!1,get:function get(){return this._writableState.length}}),Object.defineProperty(Writable.prototype,"destroyed",{enumerable:!1,get:function get(){return void 0!==this._writableState&&this._writableState.destroyed},set:function set(s){this._writableState&&(this._writableState.destroyed=s)}}),Writable.prototype.destroy=L.destroy,Writable.prototype._undestroy=L.undestroy,Writable.prototype._destroy=function(s,o){o(s)}},16946:(s,o,i)=>{"use strict";var a=i(1907),u=i(98828),_=i(45807),w=Object,x=a("".split);s.exports=u((function(){return!w("z").propertyIsEnumerable(0)}))?function(s){return"String"===_(s)?x(s,""):w(s)}:w},16962:(s,o)=>{o.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},o.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},o.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},o.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},o.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},o.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},o.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},o.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},o.realToAlias=function(){var s=Object.prototype.hasOwnProperty,i=o.aliasToReal,a={};for(var u in i){var _=i[u];s.call(a,_)?a[_].push(u):a[_]=[u]}return a}(),o.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},o.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},o.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},17255:(s,o,i)=>{var a=i(47422);s.exports=function basePropertyDeep(s){return function(o){return a(o,s)}}},17285:s=>{function source(s){return s?"string"==typeof s?s:s.source:null}function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>source(s))).join("")}function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}s.exports=function xml(s){const o=concat(/[A-Z_]/,function optional(s){return concat("(",s,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},u=s.inherit(a,{begin:/\(/,end:/\)/}),_=s.inherit(s.APOS_STRING_MODE,{className:"meta-string"}),w=s.inherit(s.QUOTE_STRING_MODE,{className:"meta-string"}),x={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[a,w,_,u,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[a,u,w,_]}]}]},s.COMMENT(//,{relevance:10}),{begin://,relevance:10},i,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[x],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[x],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:o,relevance:0,starts:x}]},{className:"tag",begin:concat(/<\//,lookahead(concat(o,/>/))),contains:[{className:"name",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17400:(s,o,i)=>{var a=i(99374),u=1/0;s.exports=function toFinite(s){return s?(s=a(s))===u||s===-1/0?17976931348623157e292*(s<0?-1:1):s==s?s:0:0===s?s:0}},17533:s=>{s.exports=function yaml(s){var o="true false yes no null",i="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},u=s.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},w={end:",",endsWithParent:!0,excludeEnd:!0,keywords:o,relevance:0},x={begin:/\{/,end:/\}/,contains:[w],illegal:"\\n",relevance:0},C={begin:"\\[",end:"\\]",contains:[w],illegal:"\\n",relevance:0},j=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+i},{className:"type",begin:"!<"+i+">"},{className:"type",begin:"!"+i},{className:"type",begin:"!!"+i},{className:"meta",begin:"&"+s.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+s.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:o,keywords:{literal:o}},_,{className:"number",begin:s.C_NUMBER_RE+"\\b",relevance:0},x,C,a],L=[...j];return L.pop(),L.push(u),w.contains=L,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:j}}},17670:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheDelete(s){var o=a(this,s).delete(s);return this.size-=o?1:0,o}},17965:(s,o,i)=>{"use strict";var a=i(16426),u={"text/plain":"Text","text/html":"Url",default:"Text"};s.exports=function copy(s,o){var i,_,w,x,C,j,L=!1;o||(o={}),i=o.debug||!1;try{if(w=a(),x=document.createRange(),C=document.getSelection(),(j=document.createElement("span")).textContent=s,j.ariaHidden="true",j.style.all="unset",j.style.position="fixed",j.style.top=0,j.style.clip="rect(0, 0, 0, 0)",j.style.whiteSpace="pre",j.style.webkitUserSelect="text",j.style.MozUserSelect="text",j.style.msUserSelect="text",j.style.userSelect="text",j.addEventListener("copy",(function(a){if(a.stopPropagation(),o.format)if(a.preventDefault(),void 0===a.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=u[o.format]||u.default;window.clipboardData.setData(_,s)}else a.clipboardData.clearData(),a.clipboardData.setData(o.format,s);o.onCopy&&(a.preventDefault(),o.onCopy(a.clipboardData))})),document.body.appendChild(j),x.selectNodeContents(j),C.addRange(x),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");L=!0}catch(a){i&&console.error("unable to copy using execCommand: ",a),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(o.format||"text",s),o.onCopy&&o.onCopy(window.clipboardData),L=!0}catch(a){i&&console.error("unable to copy using clipboardData: ",a),i&&console.error("falling back to prompt"),_=function format(s){var o=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return s.replace(/#{\s*key\s*}/g,o)}("message"in o?o.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,s)}}finally{C&&("function"==typeof C.removeRange?C.removeRange(x):C.removeAllRanges()),j&&document.body.removeChild(j),w()}return L}},18073:(s,o,i)=>{var a=i(85087),u=i(54641),_=i(70981);s.exports=function createRecurry(s,o,i,w,x,C,j,L,B,$){var V=8&o;o|=V?32:64,4&(o&=~(V?64:32))||(o&=-4);var U=[s,o,x,V?C:void 0,V?j:void 0,V?void 0:C,V?void 0:j,L,B,$],z=i.apply(void 0,U);return a(s)&&u(z,U),z.placeholder=w,_(z,s,o)}},19123:(s,o,i)=>{var a=i(65606),u=i(31499),_=i(88310).Stream;function resolve(s,o,i){var a,_=function create_indent(s,o){return new Array(o||0).join(s||"")}(o,i=i||0),w=s;if("object"==typeof s&&((w=s[a=Object.keys(s)[0]])&&w._elem))return w._elem.name=a,w._elem.icount=i,w._elem.indent=o,w._elem.indents=_,w._elem.interrupt=w,w._elem;var x,C=[],j=[];function get_attributes(s){Object.keys(s).forEach((function(o){C.push(function attribute(s,o){return s+'="'+u(o)+'"'}(o,s[o]))}))}switch(typeof w){case"object":if(null===w)break;w._attr&&get_attributes(w._attr),w._cdata&&j.push(("/g,"]]]]>")+"]]>"),w.forEach&&(x=!1,j.push(""),w.forEach((function(s){"object"==typeof s?"_attr"==Object.keys(s)[0]?get_attributes(s._attr):j.push(resolve(s,o,i+1)):(j.pop(),x=!0,j.push(u(s)))})),x||j.push(""));break;default:j.push(u(w))}return{name:a,interrupt:!1,attributes:C,content:j,icount:i,indents:_,indent:o}}function format(s,o,i){if("object"!=typeof o)return s(!1,o);var a=o.interrupt?1:o.content.length;function proceed(){for(;o.content.length;){var u=o.content.shift();if(void 0!==u){if(interrupt(u))return;format(s,u)}}s(!1,(a>1?o.indents:"")+(o.name?"":"")+(o.indent&&!i?"\n":"")),i&&i()}function interrupt(o){return!!o.interrupt&&(o.interrupt.append=s,o.interrupt.end=proceed,o.interrupt=!1,s(!0),!0)}if(s(!1,o.indents+(o.name?"<"+o.name:"")+(o.attributes.length?" "+o.attributes.join(" "):"")+(a?o.name?">":"":o.name?"/>":"")+(o.indent&&a>1?"\n":"")),!a)return s(!1,o.indent?"\n":"");interrupt(o)||proceed()}s.exports=function xml(s,o){"object"!=typeof o&&(o={indent:o});var i=o.stream?new _:null,u="",w=!1,x=o.indent?!0===o.indent?" ":o.indent:"",C=!0;function delay(s){C?a.nextTick(s):s()}function append(s,o){if(void 0!==o&&(u+=o),s&&!w&&(i=i||new _,w=!0),s&&w){var a=u;delay((function(){i.emit("data",a)})),u=""}}function add(s,o){format(append,resolve(s,x,x?1:0),o)}function end(){if(i){var s=u;delay((function(){i.emit("data",s),i.emit("end"),i.readable=!1,i.emit("close")}))}}return delay((function(){C=!1})),o.declaration&&function addXmlDeclaration(s){var o={version:"1.0",encoding:s.encoding||"UTF-8"};s.standalone&&(o.standalone=s.standalone),add({"?xml":{_attr:o}}),u=u.replace("/>","?>")}(o.declaration),s&&s.forEach?s.forEach((function(o,i){var a;i+1===s.length&&(a=end),add(o,a)})):add(s,end),i?(i.readable=!0,i):u},s.exports.element=s.exports.Element=function element(){var s={_elem:resolve(Array.prototype.slice.call(arguments)),push:function(s){if(!this.append)throw new Error("not assigned to a parent!");var o=this,i=this._elem.indent;format(this.append,resolve(s,i,this._elem.icount+(i?1:0)),(function(){o.append(!0)}))},close:function(s){void 0!==s&&this.push(s),this.end&&this.end()}};return s}},19219:s=>{s.exports=function cacheHas(s,o){return s.has(o)}},19287:s=>{"use strict";s.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},19358:(s,o,i)=>{"use strict";var a=i(85582),u=i(49724),_=i(61626),w=i(88280),x=i(79192),C=i(19595),j=i(54829),L=i(34084),B=i(32096),$=i(39259),V=i(85884),U=i(39447),z=i(7376);s.exports=function(s,o,i,Y){var Z="stackTraceLimit",ee=Y?2:1,ie=s.split("."),ae=ie[ie.length-1],ce=a.apply(null,ie);if(ce){var le=ce.prototype;if(!z&&u(le,"cause")&&delete le.cause,!i)return ce;var pe=a("Error"),de=o((function(s,o){var i=B(Y?o:s,void 0),a=Y?new ce(s):new ce;return void 0!==i&&_(a,"message",i),V(a,de,a.stack,2),this&&w(le,this)&&L(a,this,de),arguments.length>ee&&$(a,arguments[ee]),a}));if(de.prototype=le,"Error"!==ae?x?x(de,pe):C(de,pe,{name:!0}):U&&Z in ce&&(j(de,ce,Z),j(de,ce,"prepareStackTrace")),C(de,ce),!z)try{le.name!==ae&&_(le,"name",ae),le.constructor=de}catch(s){}return de}}},19570:(s,o,i)=>{var a=i(37334),u=i(93243),_=i(83488),w=u?function(s,o){return u(s,"toString",{configurable:!0,enumerable:!1,value:a(o),writable:!0})}:_;s.exports=w},19595:(s,o,i)=>{"use strict";var a=i(49724),u=i(11042),_=i(13846),w=i(74284);s.exports=function(s,o,i){for(var x=u(o),C=w.f,j=_.f,L=0;L{"use strict";var a=i(23034);s.exports=a},19846:(s,o,i)=>{"use strict";var a=i(20798),u=i(98828),_=i(45951).String;s.exports=!!Object.getOwnPropertySymbols&&!u((function(){var s=Symbol("symbol detection");return!_(s)||!(Object(s)instanceof Symbol)||!Symbol.sham&&a&&a<41}))},19931:(s,o,i)=>{var a=i(31769),u=i(68090),_=i(68969),w=i(77797);s.exports=function baseUnset(s,o){return o=a(o,s),null==(s=_(s,o))||delete s[w(u(o))]}},20181:(s,o,i)=>{var a=/^\s+|\s+$/g,u=/^[-+]0x[0-9a-f]+$/i,_=/^0b[01]+$/i,w=/^0o[0-7]+$/i,x=parseInt,C="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,j="object"==typeof self&&self&&self.Object===Object&&self,L=C||j||Function("return this")(),B=Object.prototype.toString,$=Math.max,V=Math.min,now=function(){return L.Date.now()};function isObject(s){var o=typeof s;return!!s&&("object"==o||"function"==o)}function toNumber(s){if("number"==typeof s)return s;if(function isSymbol(s){return"symbol"==typeof s||function isObjectLike(s){return!!s&&"object"==typeof s}(s)&&"[object Symbol]"==B.call(s)}(s))return NaN;if(isObject(s)){var o="function"==typeof s.valueOf?s.valueOf():s;s=isObject(o)?o+"":o}if("string"!=typeof s)return 0===s?s:+s;s=s.replace(a,"");var i=_.test(s);return i||w.test(s)?x(s.slice(2),i?2:8):u.test(s)?NaN:+s}s.exports=function debounce(s,o,i){var a,u,_,w,x,C,j=0,L=!1,B=!1,U=!0;if("function"!=typeof s)throw new TypeError("Expected a function");function invokeFunc(o){var i=a,_=u;return a=u=void 0,j=o,w=s.apply(_,i)}function shouldInvoke(s){var i=s-C;return void 0===C||i>=o||i<0||B&&s-j>=_}function timerExpired(){var s=now();if(shouldInvoke(s))return trailingEdge(s);x=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-C);return B?V(i,_-(s-j)):i}(s))}function trailingEdge(s){return x=void 0,U&&a?invokeFunc(s):(a=u=void 0,w)}function debounced(){var s=now(),i=shouldInvoke(s);if(a=arguments,u=this,C=s,i){if(void 0===x)return function leadingEdge(s){return j=s,x=setTimeout(timerExpired,o),L?invokeFunc(s):w}(C);if(B)return x=setTimeout(timerExpired,o),invokeFunc(C)}return void 0===x&&(x=setTimeout(timerExpired,o)),w}return o=toNumber(o)||0,isObject(i)&&(L=!!i.leading,_=(B="maxWait"in i)?$(toNumber(i.maxWait)||0,o):_,U="trailing"in i?!!i.trailing:U),debounced.cancel=function cancel(){void 0!==x&&clearTimeout(x),j=0,a=C=u=x=void 0},debounced.flush=function flush(){return void 0===x?w:trailingEdge(now())},debounced}},20317:s=>{s.exports=function mapToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s,a){i[++o]=[a,s]})),i}},20334:(s,o,i)=>{"use strict";var a=i(48287).Buffer;class NonError extends Error{constructor(s){super(NonError._prepareSuperMessage(s)),Object.defineProperty(this,"name",{value:"NonError",configurable:!0,writable:!0}),Error.captureStackTrace&&Error.captureStackTrace(this,NonError)}static _prepareSuperMessage(s){try{return JSON.stringify(s)}catch{return String(s)}}}const u=[{property:"name",enumerable:!1},{property:"message",enumerable:!1},{property:"stack",enumerable:!1},{property:"code",enumerable:!0}],_=Symbol(".toJSON called"),destroyCircular=({from:s,seen:o,to_:i,forceEnumerable:w,maxDepth:x,depth:C})=>{const j=i||(Array.isArray(s)?[]:{});if(o.push(s),C>=x)return j;if("function"==typeof s.toJSON&&!0!==s[_])return(s=>{s[_]=!0;const o=s.toJSON();return delete s[_],o})(s);for(const[i,u]of Object.entries(s))"function"==typeof a&&a.isBuffer(u)?j[i]="[object Buffer]":"function"!=typeof u&&(u&&"object"==typeof u?o.includes(s[i])?j[i]="[Circular]":(C++,j[i]=destroyCircular({from:s[i],seen:o.slice(),forceEnumerable:w,maxDepth:x,depth:C})):j[i]=u);for(const{property:o,enumerable:i}of u)"string"==typeof s[o]&&Object.defineProperty(j,o,{value:s[o],enumerable:!!w||i,configurable:!0,writable:!0});return j};s.exports={serializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;return"object"==typeof s&&null!==s?destroyCircular({from:s,seen:[],forceEnumerable:!0,maxDepth:i,depth:0}):"function"==typeof s?`[Function: ${s.name||"anonymous"}]`:s},deserializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;if(s instanceof Error)return s;if("object"==typeof s&&null!==s&&!Array.isArray(s)){const o=new Error;return destroyCircular({from:s,seen:[],to_:o,maxDepth:i,depth:0}),o}return new NonError(s)}}},20426:s=>{var o=Object.prototype.hasOwnProperty;s.exports=function baseHas(s,i){return null!=s&&o.call(s,i)}},20575:(s,o,i)=>{"use strict";var a=i(3121);s.exports=function(s){return a(s.length)}},20798:(s,o,i)=>{"use strict";var a,u,_=i(45951),w=i(96794),x=_.process,C=_.Deno,j=x&&x.versions||C&&C.version,L=j&&j.v8;L&&(u=(a=L.split("."))[0]>0&&a[0]<4?1:+(a[0]+a[1])),!u&&w&&(!(a=w.match(/Edge\/(\d+)/))||a[1]>=74)&&(a=w.match(/Chrome\/(\d+)/))&&(u=+a[1]),s.exports=u},20850:(s,o,i)=>{"use strict";s.exports=i(46076)},20999:(s,o,i)=>{var a=i(69302),u=i(36800);s.exports=function createAssigner(s){return a((function(o,i){var a=-1,_=i.length,w=_>1?i[_-1]:void 0,x=_>2?i[2]:void 0;for(w=s.length>3&&"function"==typeof w?(_--,w):void 0,x&&u(i[0],i[1],x)&&(w=_<3?void 0:w,_=1),o=Object(o);++a<_;){var C=i[a];C&&s(o,C,a,w)}return o}))}},21549:(s,o,i)=>{var a=i(22032),u=i(63862),_=i(66721),w=i(12749),x=i(35749);function Hash(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o{var a=i(16547),u=i(43360);s.exports=function copyObject(s,o,i,_){var w=!i;i||(i={});for(var x=-1,C=o.length;++x{var a=i(51873),u=i(37828),_=i(75288),w=i(25911),x=i(20317),C=i(84247),j=a?a.prototype:void 0,L=j?j.valueOf:void 0;s.exports=function equalByTag(s,o,i,a,j,B,$){switch(i){case"[object DataView]":if(s.byteLength!=o.byteLength||s.byteOffset!=o.byteOffset)return!1;s=s.buffer,o=o.buffer;case"[object ArrayBuffer]":return!(s.byteLength!=o.byteLength||!B(new u(s),new u(o)));case"[object Boolean]":case"[object Date]":case"[object Number]":return _(+s,+o);case"[object Error]":return s.name==o.name&&s.message==o.message;case"[object RegExp]":case"[object String]":return s==o+"";case"[object Map]":var V=x;case"[object Set]":var U=1&a;if(V||(V=C),s.size!=o.size&&!U)return!1;var z=$.get(s);if(z)return z==o;a|=2,$.set(s,o);var Y=w(V(s),V(o),a,j,B,$);return $.delete(s),Y;case"[object Symbol]":if(L)return L.call(s)==L.call(o)}return!1}},22032:(s,o,i)=>{var a=i(81042);s.exports=function hashClear(){this.__data__=a?a(null):{},this.size=0}},22225:s=>{var o="\\ud800-\\udfff",i="\\u2700-\\u27bf",a="a-z\\xdf-\\xf6\\xf8-\\xff",u="A-Z\\xc0-\\xd6\\xd8-\\xde",_="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",w="["+_+"]",x="\\d+",C="["+i+"]",j="["+a+"]",L="[^"+o+_+x+i+a+u+"]",B="(?:\\ud83c[\\udde6-\\uddff]){2}",$="[\\ud800-\\udbff][\\udc00-\\udfff]",V="["+u+"]",U="(?:"+j+"|"+L+")",z="(?:"+V+"|"+L+")",Y="(?:['’](?:d|ll|m|re|s|t|ve))?",Z="(?:['’](?:D|LL|M|RE|S|T|VE))?",ee="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",ie="[\\ufe0e\\ufe0f]?",ae=ie+ee+("(?:\\u200d(?:"+["[^"+o+"]",B,$].join("|")+")"+ie+ee+")*"),ce="(?:"+[C,B,$].join("|")+")"+ae,le=RegExp([V+"?"+j+"+"+Y+"(?="+[w,V,"$"].join("|")+")",z+"+"+Z+"(?="+[w,V+U,"$"].join("|")+")",V+"?"+U+"+"+Y,V+"+"+Z,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",x,ce].join("|"),"g");s.exports=function unicodeWords(s){return s.match(le)||[]}},22551:(s,o,i)=>{"use strict";var a=i(96540),u=i(69982);function p(s){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+s,i=1;i