Revert "Removed unused endpoints."

This reverts commit 281575bc51.
This commit is contained in:
flash 2024-08-28 20:16:33 +00:00
parent f860ca0287
commit 52903d216a
29 changed files with 1917 additions and 9 deletions

View file

@ -5,6 +5,8 @@
}
},
"require": {
"flashwave/index": "^0.2408.40014"
"flashwave/index": "^0.2408.40014",
"flashwave/syokuhou": "^1.2",
"sentry/sdk": "^4.0"
}
}

701
composer.lock generated
View file

@ -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": [],

View file

@ -1,11 +1,14 @@
<?php
namespace Satori;
use Index\Data\DbTools;
use Syokuhou\SharpConfig;
define('SAT_ROOT', __DIR__);
define('SAT_DIR_MIGRATIONS', SAT_ROOT . '/database');
define('SAT_DIR_PUBLIC', SAT_ROOT . '/public');
define('SAT_DEBUG', is_file(SAT_ROOT . '/.debug'));
define('SAT_VERSION', '20240826');
define('SAT_VERSION', '20240805');
require_once SAT_ROOT . '/vendor/autoload.php';
@ -13,4 +16,22 @@ error_reporting(SAT_DEBUG ? -1 : 0);
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
$sat = new SatoriContext;
$cfg = SharpConfig::fromFile(SAT_ROOT . '/config.cfg');
if($cfg->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);

View file

@ -0,0 +1,30 @@
<?php
namespace Satori\Booru;
use Syokuhou\IConfig;
class BooruContext {
private array $sources = [];
public function __construct(private IConfig $config) {
$this->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;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Satori\Booru;
class BooruErrorResponse implements \JsonSerializable {
public function __construct(
private string $code
) {}
public function getCode(): string {
return $this->code;
}
public function jsonSerialize(): mixed {
return ['error' => $this->code];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Satori\Booru;
class BooruQueryResponse implements \JsonSerializable {
public function __construct(
private BooruSourceInfo $source,
private array $posts
) {}
public function getSource(): BooruSourceInfo {
return $this->source;
}
public function getPosts(): array {
return $this->posts;
}
public function jsonSerialize(): mixed {
return [
'source' => $this->source,
'posts' => $this->posts,
];
}
}

59
src/Booru/BooruRoutes.php Normal file
View file

@ -0,0 +1,59 @@
<?php
namespace Satori\Booru;
use stdClass;
use Index\XArray;
use Index\Http\Routing\{HttpGet,RouteHandler};
class BooruRoutes extends RouteHandler {
public function __construct(
private BooruContext $context
) {}
private static function makePostsJsonReady(array $posts): array {
return XArray::select($posts, function($post) {
$output = new stdClass;
if($post instanceof IBooruPost) {
$output->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))
);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Satori\Booru;
class BooruSourceInfo implements \JsonSerializable {
public function __construct(
private string $name,
private string $title
) {}
public function getName(): string {
return $this->name;
}
public function getTitle(): string {
return $this->title;
}
public function jsonSerialize(): mixed {
return [
'name' => $this->name,
'type' => $this->name,
'title' => $this->title,
];
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Satori\Booru\Danbooru;
use Satori\Booru\IBooruPostWithSpecificTags;
class DanbooruPost implements IBooruPostWithSpecificTags {
public function __construct(private array $info) {}
public function getId(): string {
return (string)$this->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']);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Satori\Booru\Danbooru;
use Index\XArray;
use Syokuhou\IConfig;
use Satori\Booru\BooruSourceInfo;
use Satori\Booru\IBooruSource;
class DanbooruSource implements IBooruSource {
private const POSTS_URL = 'https://danbooru.donmai.us/posts.json';
private BooruSourceInfo $info;
public function __construct(private IConfig $config) {
$this->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));
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Satori\Booru\Gelbooru;
use Satori\Booru\IBooruPost;
class GelbooruPost implements IBooruPost {
public function __construct(private array $info) {}
public function getId(): string {
return (string)$this->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'];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Satori\Booru\Gelbooru;
use Index\XArray;
use Syokuhou\IConfig;
use Satori\Booru\BooruSourceInfo;
use Satori\Booru\IBooruSource;
class GelbooruSource implements IBooruSource {
private const POSTS_URL = 'https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1';
private BooruSourceInfo $info;
public function __construct(private IConfig $config) {
$this->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));
}
}

12
src/Booru/IBooruPost.php Normal file
View file

@ -0,0 +1,12 @@
<?php
namespace Satori\Booru;
interface IBooruPost {
public function getId(): string;
public function getRating(): string;
public function getPostUrl(): string;
public function getTags(): array;
public function hasFileUrl(): bool;
public function getFileUrl(): string;
}

View file

@ -0,0 +1,10 @@
<?php
namespace Satori\Booru;
interface IBooruPostWithSpecificTags extends IBooruPost {
public function getGeneralTags(): array;
public function getCharacterTags(): array;
public function getCopyrightTags(): array;
public function getArtistTags(): array;
public function getMetaTags(): array;
}

View file

@ -0,0 +1,7 @@
<?php
namespace Satori\Booru;
interface IBooruSource {
public function getInfo(): BooruSourceInfo;
public function getPosts(string $tags, bool $random = true, int $limit = 20): array;
}

View file

@ -0,0 +1,37 @@
<?php
namespace Satori\Booru\Konachan;
use Satori\Booru\IBooruPost;
class KonachanPost implements IBooruPost {
public function __construct(private array $info) {}
public function getId(): string {
return (string)$this->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'];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Satori\Booru\Konachan;
use Index\XArray;
use Syokuhou\IConfig;
use Satori\Booru\BooruSourceInfo;
use Satori\Booru\IBooruSource;
class KonachanSource implements IBooruSource {
private const POSTS_URL = 'https://konachan.com/post.json?api_version=2';
private BooruSourceInfo $info;
public function __construct(private IConfig $config) {
$this->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));
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Satori\Booru\Yandere;
use Satori\Booru\IBooruPost;
class YanderePost implements IBooruPost {
public function __construct(private array $info) {}
public function getId(): string {
return (string)$this->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'];
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Satori\Booru\Yandere;
use Index\XArray;
use Syokuhou\IConfig;
use Satori\Booru\BooruSourceInfo;
use Satori\Booru\IBooruSource;
class YandereSource implements IBooruSource {
private const POSTS_URL = 'https://yande.re/post.json?api_version=2';
private BooruSourceInfo $info;
public function __construct(private IConfig $config) {
$this->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));
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Satori\Dictionary;
use Syokuhou\IConfig;
use Satori\SHttp;
class DictionaryContext {
private IConfig $config;
public function __construct(IConfig $config) {
$this->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;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Satori\Dictionary;
class DictionaryResult {
public function __construct(private array $info) {}
public function getWord(): string {
return $this->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'] ?? '', [
'<em>' => '[i]',
'</em>' => '[/i]',
]);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Satori\Dictionary;
use Index\Http\Routing\{HttpGet,RouteHandler};
class DictionaryRoutes extends RouteHandler {
public function __construct(
private DictionaryContext $context
) {}
#[HttpGet('/dictionary/define')]
public function getDefine($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(),
'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;
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Satori\ExRate;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
use Satori\SHttp;
class ExRateContext {
private const BASE_CURRENCY = 'EUR';
private const COMMON_CURRENCIES = [
'EUR', 'AUD', 'GBP', 'CAD', 'USD',
'JPY', 'PLN', 'SGD', 'RUB', 'ILS',
];
private IConfig $config;
private ExRateDatabase $database;
public function __construct(IConfig $config, IDbConnection $dbConn) {
$this->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);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Satori\ExRate;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
class ExRateDatabase {
private DbStatementCache $cache;
public function __construct(
private string $baseCurrency,
private IDbConnection $dbConn
) {
$this->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;
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Satori\ExRate;
use stdClass;
use Index\Http\Routing\{HttpGet,RouteHandler};
class ExRateRoutes extends RouteHandler {
public function __construct(
private ExRateContext $context
) {}
#[HttpGet('/exrate/convert')]
public function getConvert($response, $request) {
$from = strtoupper((string)$request->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;
}
}

View file

@ -1,10 +1,65 @@
<?php
namespace Satori;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
final class SatoriContext {
private IConfig $config;
private DatabaseContext $dbCtx;
public function __construct(IConfig $config, IDbConnection $dbConn) {
$this->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;
}

View file

@ -1,15 +1,21 @@
<?php
namespace Satori\Splatoon;
use Syokuhou\IConfig;
class SplatoonContext {
private array $games = [];
public function __construct() {
public function __construct(private IConfig $config) {
$this->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;
}

View file

@ -0,0 +1,85 @@
<?php
namespace Satori\Translation;
use stdClass;
use Syokuhou\IConfig;
class TranslateContext {
public const GOOGLE_TRANSLATE = 'https://translate.google.com/translate_a/single?ie=UTF-8&oe=UTF-8&multires=1&client=gtx&sl=%s&tl=%s&dt=t&q=%s';
public const GOOGLE_USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
public const GOOGLE_LANGS = [
'af', 'sq', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs',
'bg', 'ca', 'ny', 'co', 'hr', 'cs', 'da', 'nl', 'en',
'eo', 'tl', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el',
'gu', 'ht', 'ha', 'iw', 'hi', 'hu', 'is', 'ig', 'id',
'ga', 'ja', 'jw', 'kn', 'kk', 'km', 'ko', 'ku', 'ky',
'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml',
'mt', 'mi', 'mr', 'mn', 'my', 'ne', 'no', 'ps', 'fa',
'pl', 'pt', 'ma', 'ro', 'ru', 'sm', 'gd', 'sr', 'st',
'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'su', 'sw',
'sv', 'tg', 'ta', 'te', 'th', 'tr', 'uk', 'ur', 'uz',
'vi', 'cy', 'xh', 'yi', 'yo', 'zu', 'it',
'ceb', 'haw', 'hmn',
'zh-cn', 'zh-tw',
];
private IConfig $config;
public function __construct(IConfig $config) {
$this->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;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Satori\Translation;
use DOMDocument;
use Index\Http\Routing\{HttpGet,RouteHandler};
class TranslateRoutes extends RouteHandler {
public function __construct(
private TranslateContext $context
) {}
#[HttpGet('/translate.php')]
public function getPHP($response, $request) {
$format = (string)($request->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 '';
}
}