diff --git a/composer.json b/composer.json index d94eb45..694a602 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,8 @@ } }, "require": { - "flashwave/index": "^0.2408.40014" + "flashwave/index": "^0.2408.40014", + "flashwave/syokuhou": "^1.2", + "sentry/sdk": "^4.0" } } diff --git a/composer.lock b/composer.lock index 7eb5239..efe869f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,15 +4,15 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d5cb22679bb951d16ca033d549b45f20", + "content-hash": "25cd4c9695654bac1ffda2ab6d6033de", "packages": [ { "name": "flashwave/index", - "version": "v0.2408.182001", + "version": "v0.2408.40014", "source": { "type": "git", "url": "https://patchii.net/flash/index.git", - "reference": "5a1fdcccedf818897a3468d5457875fabfb2ce28" + "reference": "fbca708fbd75e8ecc6b36b39c1307a67bf250808" }, "require": { "ext-mbstring": "*", @@ -46,7 +46,700 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2024-08-18T20:01:21+00:00" + "time": "2024-08-04T00:14:17+00:00" + }, + { + "name": "flashwave/syokuhou", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://patchii.net/flash/syokuhou.git", + "reference": "129a46c0d917382f9bc195cce278be51984eb87d" + }, + "require": { + "flashwave/index": "^0.2408.40014", + "php": ">=8.3" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Syokuhou\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "bsd-3-clause-clear" + ], + "authors": [ + { + "name": "flashwave", + "email": "packagist@flash.moe", + "homepage": "https://flash.moe", + "role": "mom" + } + ], + "description": "Configuration library for PHP.", + "homepage": "https://railgun.sh/syokuhou", + "time": "2024-08-04T01:07:23+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + }, + "time": "2024-03-08T09:58:59+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sentry/sdk", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53", + "reference": "fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53", + "shasum": "" + }, + "require": { + "sentry/sentry": "^4.0" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a meta package of sentry/sentry. We recommend using sentry/sentry directly.", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php-sdk/issues", + "source": "https://github.com/getsentry/sentry-php-sdk/tree/4.0.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-11-06T10:23:19+00:00" + }, + { + "name": "sentry/sentry", + "version": "4.8.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "61770efd8b7888e0bdd7d234f0ba67b066e47d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/61770efd8b7888e0bdd7d234f0ba67b066e47d04", + "reference": "61770efd8b7888e0bdd7d234f0ba67b066e47d04", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^1.0|^2.0", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.8.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2024-07-16T13:45:27+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "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-04-18T09:32:20+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" } ], "packages-dev": [], diff --git a/satori.php b/satori.php index bc3ea0c..e91c85f 100644 --- a/satori.php +++ b/satori.php @@ -1,11 +1,14 @@ hasValues('sentry:dsn')) + (function($cfg) { + \Sentry\init([ + 'dsn' => $cfg->getString('dsn'), + 'traces_sample_rate' => $cfg->getFloat('tracesRate', 0.2), + 'profiles_sample_rate' => $cfg->getFloat('profilesRate', 0.2), + ]); + + set_exception_handler(function(\Throwable $ex) { + \Sentry\captureException($ex); + }); + })($cfg->scopeTo('sentry')); + +$dbc = DbTools::create($cfg->getString('database:dsn', 'null')); +$dbc->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';'); + +$sat = new SatoriContext($cfg, $dbc); diff --git a/src/Booru/BooruContext.php b/src/Booru/BooruContext.php new file mode 100644 index 0000000..a4b5157 --- /dev/null +++ b/src/Booru/BooruContext.php @@ -0,0 +1,30 @@ +sources[] = new Danbooru\DanbooruSource($config->scopeTo('danbooru')); + $this->sources[] = new Gelbooru\GelbooruSource($config->scopeTo('gelbooru')); + $this->sources[] = new Konachan\KonachanSource($config->scopeTo('konachan')); + $this->sources[] = new Yandere\YandereSource($config->scopeTo('yandere')); + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getSources(): array { + return $this->sources; + } + + public function getSourceByName(string $name): ?IBooruSource { + foreach($this->sources as $source) + if($source->getInfo()->getName() === $name) + return $source; + return null; + } +} diff --git a/src/Booru/BooruErrorResponse.php b/src/Booru/BooruErrorResponse.php new file mode 100644 index 0000000..882bea6 --- /dev/null +++ b/src/Booru/BooruErrorResponse.php @@ -0,0 +1,16 @@ +code; + } + + public function jsonSerialize(): mixed { + return ['error' => $this->code]; + } +} diff --git a/src/Booru/BooruQueryResponse.php b/src/Booru/BooruQueryResponse.php new file mode 100644 index 0000000..42120ec --- /dev/null +++ b/src/Booru/BooruQueryResponse.php @@ -0,0 +1,24 @@ +source; + } + + public function getPosts(): array { + return $this->posts; + } + + public function jsonSerialize(): mixed { + return [ + 'source' => $this->source, + 'posts' => $this->posts, + ]; + } +} diff --git a/src/Booru/BooruRoutes.php b/src/Booru/BooruRoutes.php new file mode 100644 index 0000000..1458dc0 --- /dev/null +++ b/src/Booru/BooruRoutes.php @@ -0,0 +1,59 @@ +id = (int)$post->getId(); + $output->rating = $post->getRating(); + if($post->hasFileUrl()) + $output->file_url = $post->getFileUrl(); + $output->post_url = $post->getPostUrl(); + $output->tags = $post->getTags(); + + if($post instanceof IBooruPostWithSpecificTags) { + $output->tags_general = $post->getGeneralTags(); + $output->tags_character = $post->getCharacterTags(); + $output->tags_copyright = $post->getCopyrightTags(); + $output->tags_artist = $post->getArtistTags(); + $output->tags_meta = $post->getMetaTags(); + } + } + + return $output; + }); + } + + #[HttpGet('/booru.php')] + public function getPHP($response, $request) { + $sourceName = (string)$request->getParam('b'); + $tags = (string)$request->getParam('t'); + + if($sourceName === '') { + $sources = $this->context->getSources(); + if(empty($sources)) + return new BooruErrorResponse('none'); + $source = $sources[0]; + } else + $source = $this->context->getSourceByName($sourceName); + + if($source === null) + return new BooruErrorResponse('booru'); + + return new BooruQueryResponse( + $source->getInfo(), + self::makePostsJsonReady($source->getPosts($tags)) + ); + } +} diff --git a/src/Booru/BooruSourceInfo.php b/src/Booru/BooruSourceInfo.php new file mode 100644 index 0000000..9f8cfdb --- /dev/null +++ b/src/Booru/BooruSourceInfo.php @@ -0,0 +1,25 @@ +name; + } + + public function getTitle(): string { + return $this->title; + } + + public function jsonSerialize(): mixed { + return [ + 'name' => $this->name, + 'type' => $this->name, + 'title' => $this->title, + ]; + } +} diff --git a/src/Booru/Danbooru/DanbooruPost.php b/src/Booru/Danbooru/DanbooruPost.php new file mode 100644 index 0000000..86704e9 --- /dev/null +++ b/src/Booru/Danbooru/DanbooruPost.php @@ -0,0 +1,52 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating']; + } + + public function getPostUrl(): string { + return sprintf('https://danbooru.donmai.us/posts/%d', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tag_string']); + } + + public function hasFileUrl(): bool { + return array_key_exists('file_url', $this->info); + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } + + public function getGeneralTags(): array { + return explode(' ', $this->info['tag_string_general']); + } + + public function getCharacterTags(): array { + return explode(' ', $this->info['tag_string_character']); + } + + public function getCopyrightTags(): array { + return explode(' ', $this->info['tag_string_copyright']); + } + + public function getArtistTags(): array { + return explode(' ', $this->info['tag_string_artist']); + } + + public function getMetaTags(): array { + return explode(' ', $this->info['tag_string_meta']); + } +} diff --git a/src/Booru/Danbooru/DanbooruSource.php b/src/Booru/Danbooru/DanbooruSource.php new file mode 100644 index 0000000..af1f2aa --- /dev/null +++ b/src/Booru/Danbooru/DanbooruSource.php @@ -0,0 +1,63 @@ +info = new BooruSourceInfo('danbooru', 'Danbooru'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + if($random) + $tags .= ' order:random'; + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + + $headers = ['Accept: application/json']; + if($this->config->hasValues('token')) + $headers[] = sprintf('Authorization: Basic %s', base64_encode($this->config->getString('token'))); + + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => $headers, + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + return empty($response) ? [] : XArray::select($response, fn($post) => new DanbooruPost($post)); + } +} diff --git a/src/Booru/Gelbooru/GelbooruPost.php b/src/Booru/Gelbooru/GelbooruPost.php new file mode 100644 index 0000000..7e4408c --- /dev/null +++ b/src/Booru/Gelbooru/GelbooruPost.php @@ -0,0 +1,32 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating'][0]; + } + + public function getPostUrl(): string { + return sprintf('https://gelbooru.com/index.php?page=post&s=view&id=%d', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tags']); + } + + public function hasFileUrl(): bool { + return array_key_exists('file_url', $this->info); + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } +} diff --git a/src/Booru/Gelbooru/GelbooruSource.php b/src/Booru/Gelbooru/GelbooruSource.php new file mode 100644 index 0000000..fdb3410 --- /dev/null +++ b/src/Booru/Gelbooru/GelbooruSource.php @@ -0,0 +1,59 @@ +info = new BooruSourceInfo('gelbooru', 'Gelbooru'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + if($random) + $tags .= ' sort:random'; + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + return empty($response) || empty($response['post']) || !is_array($response['post']) + ? [] : XArray::select($response['post'], fn($post) => new GelbooruPost($post)); + } +} diff --git a/src/Booru/IBooruPost.php b/src/Booru/IBooruPost.php new file mode 100644 index 0000000..74b5be1 --- /dev/null +++ b/src/Booru/IBooruPost.php @@ -0,0 +1,12 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating']; + } + + public function isSafe(): bool { + return $this->info['rating'] === 'g' + || $this->info['rating'] === 's'; + } + + public function getPostUrl(): string { + return sprintf('https://konachan.%s/post/show/%d', $this->isSafe() ? 'net' : 'com', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tags']); + } + + public function hasFileUrl(): bool { + return array_key_exists('file_url', $this->info); + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } +} diff --git a/src/Booru/Konachan/KonachanSource.php b/src/Booru/Konachan/KonachanSource.php new file mode 100644 index 0000000..6a3b429 --- /dev/null +++ b/src/Booru/Konachan/KonachanSource.php @@ -0,0 +1,59 @@ +info = new BooruSourceInfo('konachan', 'Konachan'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + if($random) + $tags .= ' order:random'; + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + return empty($response) || empty($response['posts']) || !is_array($response['posts']) + ? [] : XArray::select($response['posts'], fn($post) => new KonachanPost($post)); + } +} diff --git a/src/Booru/Yandere/YanderePost.php b/src/Booru/Yandere/YanderePost.php new file mode 100644 index 0000000..64d5c0b --- /dev/null +++ b/src/Booru/Yandere/YanderePost.php @@ -0,0 +1,34 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating']; + } + + public function getPostUrl(): string { + return sprintf('https://yande.re/post/show/%d', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tags']); + } + + public function hasFileUrl(): bool { + // yandere images get fucking massive sometimes + // and make the upload process timeout and silently fail + return false; + } + + public function getFileUrl(): string { + return $this->info['sample_url']; + } +} diff --git a/src/Booru/Yandere/YandereSource.php b/src/Booru/Yandere/YandereSource.php new file mode 100644 index 0000000..4d17df1 --- /dev/null +++ b/src/Booru/Yandere/YandereSource.php @@ -0,0 +1,78 @@ +info = new BooruSourceInfo('yandere', 'Yandere'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + $localRandom = false; + $localLimit = $limit; + + if($random) { + $localRandom = trim($tags) !== ''; + + if($localRandom) + $limit = max($limit, 200); + else + $tags = 'order:random'; + } + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + if(empty($response) || empty($response['posts']) || !is_array($response['posts'])) + return []; + + $posts = $response['posts']; + + if($localRandom) { + shuffle($posts); + if($localLimit > 0) + $posts = array_slice($posts, 0, $localLimit); + } + + return XArray::select($posts, fn($post) => new YanderePost($post)); + } +} diff --git a/src/Dictionary/DictionaryContext.php b/src/Dictionary/DictionaryContext.php new file mode 100644 index 0000000..f7ddc6a --- /dev/null +++ b/src/Dictionary/DictionaryContext.php @@ -0,0 +1,35 @@ +config = $config; + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function defineWord(string $word): array { + $data = SHttp::getJson(sprintf( + 'https://api.wordnik.com/v4/word.json/%s/definitions?limit=5&includeRelated=false&sourceDictionaries=ahd-5%%2Cwordnet&useCanonical=true&includeTags=false&api_key=%s', + rawurlencode($word), + $this->config->getString('token') + )); + + if(empty($data) || !empty($data['statusCode'])) + return []; + + $results = []; + + foreach($data as $result) + $results[] = new DictionaryResult($result); + + return $results; + } +} diff --git a/src/Dictionary/DictionaryResult.php b/src/Dictionary/DictionaryResult.php new file mode 100644 index 0000000..e0272cf --- /dev/null +++ b/src/Dictionary/DictionaryResult.php @@ -0,0 +1,42 @@ +info['word'] ?? ''; + } + + public function getPartOfSpeech(): string { + return $this->info['partOfSpeech'] ?? ''; + } + + public function getAttributionText(): string { + return $this->info['attributionText'] ?? ''; + } + + public function getAttributionUrl(): string { + return $this->info['attributionUrl'] ?? ''; + } + + public function getSourceDictionary(): string { + return $this->info['sourceDictionary'] ?? ''; + } + + public function getWordnikUrl(): string { + return $this->info['wordnikUrl'] ?? ''; + } + + public function hasText(): bool { + return array_key_exists('text', $this->info) + && $this->info['text'] !== ''; + } + + public function getText(): string { + return strtr($this->info['text'] ?? '', [ + '' => '[i]', + '' => '[/i]', + ]); + } +} diff --git a/src/Dictionary/DictionaryRoutes.php b/src/Dictionary/DictionaryRoutes.php new file mode 100644 index 0000000..d60c63c --- /dev/null +++ b/src/Dictionary/DictionaryRoutes.php @@ -0,0 +1,64 @@ +getParam('word')); + if($word === '') + return ['error' => 'word']; + + $results = $this->context->defineWord($word); + $response = []; + + foreach($results as $result) { + if(!$result->hasText()) + continue; + + $response[] = [ + 'word' => $result->getWord(), + 'part_of_speech' => $result->getPartOfSpeech(), + 'attribution_text' => $result->getAttributionText(), + 'attribution_url' => $result->getAttributionUrl(), + 'source_dictionary' => $result->getSourceDictionary(), + 'text' => $result->getText(), + 'wordnik_url' => $result->getWordnikUrl(), + ]; + } + + return $response; + } + + #[HttpGet('/word-define.php')] + public function getPHP($response, $request) { + $word = trim((string)$request->getParam('word')); + if($word === '') + return ['error' => 'word']; + + $results = $this->context->defineWord($word); + $response = []; + + foreach($results as $result) { + if(!$result->hasText()) + continue; + + $response[] = [ + 'word' => $result->getWord(), + 'pos' => $result->getPartOfSpeech(), + 'attr' => $result->getAttributionText(), + 'attr_url' => $result->getAttributionUrl(), + 'dict' => $result->getSourceDictionary(), + 'text' => $result->getText(), + 'url' => $result->getWordnikUrl(), + ]; + } + + return $response; + } +} diff --git a/src/ExRate/ExRateContext.php b/src/ExRate/ExRateContext.php new file mode 100644 index 0000000..de9db83 --- /dev/null +++ b/src/ExRate/ExRateContext.php @@ -0,0 +1,69 @@ +config = $config; + $this->database = new ExRateDatabase(self::BASE_CURRENCY, $dbConn); + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getDatabase(): ExRateDatabase { + return $this->database; + } + + public function getBaseCurrency(): string { + return self::BASE_CURRENCY; + } + + public function getCommonCurrencies(): array { + return self::COMMON_CURRENCIES; + } + + public function ratesNeedRefresh(): bool { + return $this->database->ratesNeedRefresh(); + } + + public function refreshRates(): void { + // HTTPS is a paid feature????????? + $data = SHttp::getJsonCached(sprintf( + 'http://api.exchangerate.host/live?source=%s&access_key=%s', + self::BASE_CURRENCY, + $this->config->getString('token') + )); + + if(empty($data) || !$data['success']) + return; + + $this->database->clearRates(); + $this->database->insertRate(self::BASE_CURRENCY, 1.0); + + foreach($data['quotes'] as $names => $value) { + $to = substr($names, 3, 3); + if(strlen($to) !== 3) + continue; + + $this->database->insertRate($to, $value); + } + } + + public function convertRate(string $from, string $to, float $value): float { + return $this->database->convertRate($from, $to, $value); + } +} diff --git a/src/ExRate/ExRateDatabase.php b/src/ExRate/ExRateDatabase.php new file mode 100644 index 0000000..9dd3ee1 --- /dev/null +++ b/src/ExRate/ExRateDatabase.php @@ -0,0 +1,49 @@ +cache = new DbStatementCache($dbConn); + } + + public function ratesNeedRefresh(): bool { + $result = $this->dbConn->query('SELECT MAX(rate_stored) > NOW() - INTERVAL 1 DAY FROM `exchange-rates` LIMIT 1'); + return !$result->next() || $result->isNull(0) || $result->getInteger(0) < 1; + } + + public function clearRates(): void { + $this->dbConn->execute('TRUNCATE `exchange-rates`'); + } + + public function insertRate(string $to, float $rate): void { + $stmt = $this->cache->get(sprintf( + 'INSERT INTO `exchange-rates` (rate_from, rate_to, rate_value) VALUES ("%s", ?, ?)', + $this->baseCurrency + )); + $stmt->addParameter(1, $to); + $stmt->addParameter(2, $rate); + $stmt->execute(); + } + + public function convertRate(string $from, string $to, float $value): float { + $stmt = $this->cache->get(sprintf( + 'SELECT (SELECT (? / rate_value) FROM `exchange-rates` WHERE rate_from = "%1$s" AND rate_to = ?) * rate_value FROM `exchange-rates` WHERE rate_from = "%1$s" AND rate_to = ?', + $this->baseCurrency + )); + $stmt->addParameter(1, $value); + $stmt->addParameter(2, $from); + $stmt->addParameter(3, $to); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getFloat(0) : 0; + } +} diff --git a/src/ExRate/ExRateRoutes.php b/src/ExRate/ExRateRoutes.php new file mode 100644 index 0000000..b2b234a --- /dev/null +++ b/src/ExRate/ExRateRoutes.php @@ -0,0 +1,92 @@ +getParam('from')); + $targets = explode(',', strtoupper((string)$request->getParam('to'))); + $amount = (string)($request->getParam('amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? '1'); + + if(strlen($from) !== 3) + return 400; + + if(empty($targets) || empty($targets[0])) + $targets = $this->context->getCommonCurrencies(); + else + foreach($targets as $target) + if(strlen($target) !== 3) + return 400; + + if($this->context->ratesNeedRefresh()) + $this->context->refreshRates(); + + $response = new stdClass; + $response->from = $from; + $response->amount = (float)$amount; + $response->results = []; + + foreach($targets as $target) { + if($target === $from) + continue; + + $response->results[] = [ + 'to' => $target, + 'result' => $this->context->convertRate($from, $target, $amount), + ]; + } + + return $response; + } + + #[HttpGet('/exrate.php')] + public function getPHP($response, $request) { + $from = strtoupper((string)$request->getParam('from')); + $to = strtoupper((string)$request->getParam('to')); + $amount = (string)($request->getParam('amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? '1'); + + if((!empty($to) && strlen($to) !== 3) || strlen($from) !== 3) { + $response->setStatusCode(404); + return 'Invalid currency specified.'; + } + + if($this->context->ratesNeedRefresh()) + $this->context->refreshRates(); + + + $response = new stdClass; + $response->from = $from; + $response->to = $to; + $response->amount = (float)$amount; + + if($from === $to) { + $response->result = $response->amount; + return $response; + } + + $singleResult = !empty($to); + $targets = $singleResult ? [$to] : $this->context->getCommonCurrencies(); + + $results = []; + + foreach($targets as $to) + $results[] = [ + 'to' => $to, + 'result' => $this->context->convertRate($from, $to, $amount), + ]; + + if($singleResult) + $response->result = $results[0]['result']; + else + $response->results = $results; + + return $response; + } +} diff --git a/src/SatoriContext.php b/src/SatoriContext.php index e928325..f1a02c0 100644 --- a/src/SatoriContext.php +++ b/src/SatoriContext.php @@ -1,10 +1,65 @@ config = $config; + $this->dbCtx = new DatabaseContext($dbConn); + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getDatabase(): DatabaseContext { + return $this->dbCtx; + } + + public function createExRates(): ExRate\ExRateContext { + return new ExRate\ExRateContext( + $this->config->scopeTo('exrate'), + $this->dbCtx->getConnection() + ); + } + + public function createDictionary(): Dictionary\DictionaryContext { + return new Dictionary\DictionaryContext( + $this->config->scopeTo('dictionary') + ); + } + + public function createTranslation(): Translation\TranslateContext { + return new Translation\TranslateContext( + $this->config->scopeTo('translate') + ); + } + + public function createBooru(): Booru\BooruContext { + return new Booru\BooruContext( + $this->config->scopeTo('booru') + ); + } + + public function createSplatoon(): Splatoon\SplatoonContext { + return new Splatoon\SplatoonContext( + $this->config->scopeTo('splatoon') + ); + } + public function createRouting(): RoutingContext { $routingCtx = new RoutingContext; - $routingCtx->register(new Splatoon\SplatoonRoutes(new Splatoon\SplatoonContext)); + + $routingCtx->register(new Booru\BooruRoutes($this->createBooru())); + $routingCtx->register(new Dictionary\DictionaryRoutes($this->createDictionary())); + $routingCtx->register(new ExRate\ExRateRoutes($this->createExRates())); + $routingCtx->register(new Splatoon\SplatoonRoutes($this->createSplatoon())); + $routingCtx->register(new Translation\TranslateRoutes($this->createTranslation())); return $routingCtx; } diff --git a/src/Splatoon/SplatoonContext.php b/src/Splatoon/SplatoonContext.php index c0e5943..7c50967 100644 --- a/src/Splatoon/SplatoonContext.php +++ b/src/Splatoon/SplatoonContext.php @@ -1,15 +1,21 @@ games[] = new Splatoon3\Splatoon3Game; $this->games[] = new Splatoon2\Splatoon2Game; $this->games[] = new Splatoon1\Splatoon1Game; } + public function getConfig(): IConfig { + return $this->config; + } + public function getGames(): array { return $this->games; } diff --git a/src/Translation/TranslateContext.php b/src/Translation/TranslateContext.php new file mode 100644 index 0000000..4cda628 --- /dev/null +++ b/src/Translation/TranslateContext.php @@ -0,0 +1,85 @@ +config = $config; + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getGoogleLanguageCodes(): array { + return self::GOOGLE_LANGS; + } + + public function getGoogleLanguages(): array { + $langs = []; + + foreach(self::GOOGLE_LANGS as $langId) + $langs[] = [ + 'code' => $langId, + 'name' => locale_get_display_name($langId), + ]; + + return $langs; + } + + public function googleTranslate(string $from, string $to, string $text): ?object { + $curl = curl_init(sprintf( + self::GOOGLE_TRANSLATE, + rawurlencode($from), + rawurlencode($to), + rawurlencode($text) + )); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => self::GOOGLE_USERAGENT, + ]); + $response = curl_exec($curl); + curl_close($curl); + + if(empty($response)) + return null; + + $response = json_decode($response); + if(empty($response)) + return null; + + $result = new stdClass; + $result->translatedText = $response[0][0][0] ?? ''; + $result->originalText = $response[0][0][1] ?? ''; + $result->fromLangCode = $response[2] ?? ''; + $result->fromLangName = locale_get_display_name($result->fromLangCode); + $result->toLangCode = $to; + $result->toLangName = locale_get_display_name($to); + + return $result; + } +} diff --git a/src/Translation/TranslateRoutes.php b/src/Translation/TranslateRoutes.php new file mode 100644 index 0000000..0faaa0a --- /dev/null +++ b/src/Translation/TranslateRoutes.php @@ -0,0 +1,98 @@ +getParam('fmt') ?? 'json'); + if($format !== 'json' && $format !== 'xml') + return ''; + + $mode = (string)$request->getParam('m'); + + if($mode === 'list') { + $langs = $this->context->getGoogleLanguages(); + + if($format === 'xml') { + $document = new DOMDocument('1.0', 'UTF-8'); + + $array = $document->createElement('ArrayOfLanguage'); + $document->appendChild($array); + + foreach($langs as $langInfo) { + $lang = $document->createElement('Language'); + + foreach($langInfo as $name => $value) + $lang->setAttribute($name, $value); + + $array->appendChild($lang); + } + + return $document->saveXML(); + } + + return $langs; + } + + if($mode === 'do') { + $from = (string)($request->getParam('f') ?? 'auto'); + $to = (string)($request->getParam('t') ?? 'en'); + $langs = $this->context->getGoogleLanguageCodes(); + + $encodeOutput = function(array $info) use ($format) { + if($format === 'json') + return $info; + + if($format === 'xml') { + $document = new DOMDocument('1.0', 'UTF-8'); + $translation = $document->createElement('Translation'); + + foreach($info as $name => $value) + $translation->setAttribute($name, $value); + + $document->appendChild($translation); + + return $document->saveXML(); + } + + return ''; + }; + + if($from !== 'auto' && !in_array($from, $langs)) + return $encodeOutput(['error' => 'Source language is not supported.']); + if(!in_array($to, $langs)) + return $encodeOutput(['error' => 'Target language is not supported.']); + + $text = ''; + if($request->hasParam('z')) + $text = trim((string)$request->getParam('z')); + elseif($request->hasContent()) + $text = trim((string)$request->getContent()); + + if($text === '') + return $encodeOutput(['error' => 'Nothing to translate.']); + + $result = $this->context->googleTranslate($from, $to, $text); + if($result === null) + return $encodeOutput(['error' => 'Translation failed.']); + + return $encodeOutput([ + 'from' => $result->fromLangCode, + 'from_text' => $result->fromLangName, + 'to' => $result->toLangCode, + 'to_text' => $result->toLangName, + 'original' => $result->originalText, + 'result' => $result->translatedText, + ]); + } + + return ''; + } +}