Satori API scripts overhaul!

This commit is contained in:
flash 2023-11-06 00:45:15 +00:00
parent 59dba92418
commit f491be9eba
99 changed files with 3867 additions and 1704 deletions

10
.gitignore vendored
View file

@ -1,2 +1,8 @@
/config/git-broadcast.ini
/config/flashii.ini
[Tt]humbs.db
[Dd]esktop.ini
.DS_Store
/.debug
/vendor
/config.cfg
/config.ini
/.migrating

13
composer.json Normal file
View file

@ -0,0 +1,13 @@
{
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Satori\\": "src"
}
},
"require": {
"flashwave/index": "dev-master",
"flashwave/syokuhou": "dev-master"
}
}

107
composer.lock generated Normal file
View file

@ -0,0 +1,107 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d909826a501788db19148054e389ec5f",
"packages": [
{
"name": "flashwave/index",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://git.flash.moe/flash/index.git",
"reference": "82a350a5c719cc83aa22382201683a68a2629f2a"
},
"require": {
"ext-mbstring": "*",
"php": ">=8.1"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.2"
},
"suggest": {
"ext-mysqli": "Support for the Index\\Data\\MariaDB namespace (both mysqlnd and libmysql are supported).",
"ext-sqlite3": "Support for the Index\\Data\\SQLite namespace."
},
"default-branch": true,
"type": "library",
"autoload": {
"files": [
"index.php"
],
"psr-4": {
"Index\\": "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": "Composer package for the common library for my projects.",
"homepage": "https://railgun.sh/index",
"time": "2023-09-15T22:44:36+00:00"
},
{
"name": "flashwave/syokuhou",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://git.flash.moe/flash/syokuhou.git",
"reference": "b3470ad8605b0484294c73cd95be6e7ba4551e5a"
},
"require": {
"flashwave/index": "dev-master",
"php": ">=8.2"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.4"
},
"default-branch": true,
"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": "2023-10-20T21:26:38+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"flashwave/index": 20,
"flashwave/syokuhou": 20
},
"prefer-stable": true,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -0,0 +1,24 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class InitialStructure_20231106_004121 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$existingTables = [];
$result = $conn->query('SHOW TABLES');
while($result->next())
$existingTables[] = $result->getString(0);
if(!in_array('exchange-rates', $existingTables))
$conn->execute('
CREATE TABLE `exchange-rates` (
rate_from BINARY(3) NOT NULL,
rate_to BINARY(3) NOT NULL,
rate_value DECIMAL(20,6) NOT NULL,
rate_stored TIMESTAMP NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (rate_from, rate_to),
KEY rate_stored (rate_stored)
) ENGINE=InnoDB COLLATE="utf8mb4_bin"
');
}
}

47
public/404.html Normal file
View file

@ -0,0 +1,47 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Cannot find page</title>
<link rel="stylesheet" type="text/css" href="/satori.css">
</head>
<body>
<div id="wrap">
<h1>
<img src="//static.flash.moe/images/404-info.gif">
The page cannot be found
</h1>
<p>
The page you are looking for might have been removed, had its
name changed, or is temporarily unavailable.
</p>
<hr>
<h2>
Please try the following:
</h2>
<ul>
<li>
If you typed the page address in the Address bar, make
sure that it is spelled correctly.
</li>
<li>
Open the <a href="//flashii.net">flashii.net</a>
home page, and then look for links to the information you want.
</li>
<li>
Click the <img src="//static.flash.moe/images/404-back.gif"><a href="javascript:void(0);" onclick="history.go(-1);">Back</a>
button to try another link.
</li>
<li>
Click <img src="//static.flash.moe/images/404-search.gif"><a href="//flashii.net/search.php">Search</a>
to look for information on the Internet.
</li>
</ul>
<h3>
HTTP 404 - File not found
<br>
Internet Explorer
</h3>
</div>
</body>
</html>

View file

@ -1,186 +0,0 @@
<?php
define('BOORUS', [
'danbooru',
'gelbooru',
'yandere',
]);
define('BOORU_INFOS', [
'danbooru' => [
'title' => 'Danbooru',
'type' => 'danbooru',
'randomUrlFormat' => 'https://danbooru.donmai.us/posts.json?limit=20&tags=order:random+limit:20+%s',
'postUrlFormat' => 'https://danbooru.donmai.us/posts/%s',
'implicitTags' => [
'misaka_mikoto' => ['-shokuhou_misaki'],
'shokuhou_misaki' => ['-misaka_mikoto'],
'kagari_(rewrite)' => ['-screencap'],
'kasuga_ayumu' => ['-rating:explicit'],
'osaka_(azumanga_daioh)' => ['-rating:explicit'],
'yazawa_nico' => ['-nishikino_maki'],
'nishikino_maki' => ['-yazawa_nico'],
'heanna_sumire' => ['-tang_keke'],
'tang_keke' => ['-heanna_sumire'],
],
],
'gelbooru' => [
'title' => 'Gelbooru',
'type' => 'gelbooru',
'randomUrlFormat' => 'https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&limit=20&tags=sort:random+%s',
'postUrlFormat' => 'https://gelbooru.com/index.php?page=post&s=view&id=%s',
'implicitTags' => [
'misaka_mikoto' => ['-shokuhou_misaki'],
'shokuhou_misaki' => ['-misaka_mikoto'],
'kagari_(rewrite)' => ['-screencap'],
'kasuga_ayumu' => ['-rating:explicit'],
'yazawa_nico' => ['-nishikino_maki'],
'nishikino_maki' => ['-yazawa_nico'],
'heanna_sumire' => ['-tang_keke'],
'tang_keke' => ['-heanna_sumire'],
],
],
'yandere' => [
'title' => 'Yande.re',
'type' => 'yandere',
'randomUrlFormat' => 'https://yande.re/post.json?api_version=2&limit=20&tags=order:random+%s',
'postUrlFormat' => 'https://yande.re/post/show/%s',
'implicitTags' => [
'misaka_mikoto' => ['-shokuhou_misaki'],
'shokuhou_misaki' => ['-misaka_mikoto'],
'kagari_(rewrite)' => ['-cap'],
'kasuga_ayumu' => ['-rating:explicit'],
'yazawa_nico' => ['-nishikino_maki'],
'nishikino_maki' => ['-yazawa_nico'],
'heanna_sumire' => ['-tang_keke'],
'tang_keke' => ['-heanna_sumire'],
],
],
]);
$config = parse_ini_file(__DIR__ . '/../config/flashii.ini');
header('Content-Type: application/json; charset=utf-8');
$booru = (string)filter_input(INPUT_GET, 'b');
$tags = (string)filter_input(INPUT_GET, 't');
$tags = array_filter(explode(' ', trim($tags)));
$randomSource = empty($booru);
if($randomSource)
$booru = BOORUS[0];
elseif(!in_array($booru, BOORUS))
die('{"error":"booru"}');
if(!array_key_exists($booru, BOORU_INFOS))
die('{"error":"booru-info"}');
$booruInfo = BOORU_INFOS[$booru];
if(empty($booruInfo['randomUrlFormat']))
die('{"error":"random"}');
if(empty($booruInfo['postUrlFormat']))
die('{"error":"post"}');
if(empty($booruInfo['type']))
die('{"error":"type"}');
$isDanbooru = $booruInfo['type'] === 'danbooru';
$isGelbooru = $booruInfo['type'] === 'gelbooru';
$isYandere = $booruInfo['type'] === 'yandere';
if(!empty($booruInfo['implicitTags'])) {
$originalTags = $tags;
foreach($booruInfo['implicitTags'] as $targetTag => $addTags) {
if(!in_array($targetTag, $originalTags))
continue;
foreach($addTags as $tag)
if(!in_array($tag, $tags))
$tags[] = $tag;
}
}
$tagString = implode('+', array_map(fn($x) => urlencode($x), $tags));
$curl = curl_init(sprintf($booruInfo['randomUrlFormat'], $tagString));
$headers = ['Accept: application/json'];
if($isDanbooru)
$headers[] = 'Authorization: Basic ' . base64_encode($config['danbooru-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 => 'Satori/20230202 (+https://fii.moe/satori)',
CURLOPT_HTTPHEADER => $headers,
]);
$response = json_decode(curl_exec($curl));
curl_close($curl);
$posts = [];
// gelbooru and yandere block remote embedding, so not even going to bother with a file_url for those
if($isDanbooru) {
if(!empty($response) && is_array($response)) {
foreach($response as $raw) {
$posts[] = $post = new stdClass;
$post->id = $raw->id;
$post->rating = $raw->rating;
if(!empty($raw->file_url))
$post->file_url = $raw->file_url;
$post->post_url = sprintf($booruInfo['postUrlFormat'], $raw->id);
$post->tags = explode(' ', $raw->tag_string);
$post->tags_general = explode(' ', $raw->tag_string_general);
$post->tags_character = explode(' ', $raw->tag_string_character);
$post->tags_copyright = explode(' ', $raw->tag_string_copyright);
$post->tags_artist = explode(' ', $raw->tag_string_artist);
$post->tags_meta = explode(' ', $raw->tag_string_meta);
}
}
} elseif($isGelbooru) {
if(!empty($response) && !empty($response->post) && is_array($response->post)) {
foreach($response->post as $raw) {
$posts[] = $post = new stdClass;
$post->id = $raw->id;
$post->rating = $raw->rating[0];
//if(!empty($raw->file_url))
// $post->file_url = $raw->file_url;
$post->post_url = sprintf($booruInfo['postUrlFormat'], $raw->id);
$post->tags = explode(' ', $raw->tags);
}
}
} elseif($isYandere) {
if(!empty($response) && !empty($response->posts) && is_array($response->posts)) {
foreach($response->posts as $raw) {
$posts[] = $post = new stdClass;
$post->id = $raw->id;
$post->rating = $raw->rating;
//if(!empty($raw->file_url))
// $post->file_url = $raw->file_url;
$post->post_url = sprintf($booruInfo['postUrlFormat'], $raw->id);
$post->tags = explode(' ', $raw->tags);
}
}
} else
die('{"error":"support"}');
echo json_encode([
'source' => [
'name' => $booru,
'type' => $booruInfo['type'],
'title' => $booruInfo['title'] ?? $booru,
],
'tags' => $tags,
'posts' => $posts,
]);

View file

@ -1,93 +0,0 @@
<?php
$config = parse_ini_file(__DIR__ . '/../config/flashii.ini');
require_once $config['msz-path'] . '/vendor/flashwave/index/index.php';
try {
$db = \Index\Data\DbTools::create($config['exrate-dsn2']);
$db->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\';');
} catch(Exception $ex) {
die((string)$ex);
}
define('EXRATE_INTER', 'EUR');
define('EXRATE_COMMON', [
'EUR', 'AUD', 'GBP', 'CAD', 'USD', 'JPY', 'PLN', 'SGD', 'RUB', 'ILS',
]);
$from = strtoupper((string)filter_input(INPUT_GET, 'from'));
$to = strtoupper((string)filter_input(INPUT_GET, 'to'));
$amount = (string)(filter_input(INPUT_GET, 'amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? '1');
if((!empty($to) && strlen($to) !== 3) || strlen($from) !== 3) {
http_response_code(400);
die('Invalid currency specified.');
}
$needsRefresh = $db->query('SELECT MAX(rate_stored) > NOW() - INTERVAL 1 DAY FROM `exchange-rates` LIMIT 1');
$needsRefresh->next();
$needsRefresh = $needsRefresh->isNull(0) || $needsRefresh->getInteger(0) < 1;
if($needsRefresh) {
$data = json_decode(file_get_contents('https://api.exchangerate.host/latest?base=' . EXRATE_INTER), true);
if($data !== null) {
$db->execute('TRUNCATE `exchange-rates`');
$insertCurrency = $db->prepare('INSERT INTO `exchange-rates` (rate_from, rate_to, rate_value) VALUES (?, ?, ?)');
foreach($data['rates'] as $currency => $rate) {
$insertCurrency->reset();
$insertCurrency->addParameter(1, $data['base']);
$insertCurrency->addParameter(2, $currency);
$insertCurrency->addParameter(3, $rate);
$insertCurrency->execute();
}
}
}
$result = new stdClass;
$result->from = $from;
$result->to = $to;
$result->amount = (float)$amount;
if($from === $to) {
$result->result = $result->amount;
} else {
$convertCurrency = $db->prepare(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 = ?',
EXRATE_INTER
));
if(empty($to)) {
$result->results = [];
foreach(EXRATE_COMMON as $commonCurrency) {
if($commonCurrency === $from)
continue;
$result->results[] = $current = new stdClass;
$current->to = $commonCurrency;
$convertCurrency->reset();
$convertCurrency->addParameter(1, $amount);
$convertCurrency->addParameter(2, $from);
$convertCurrency->addParameter(3, $commonCurrency);
$convertCurrency->execute();
$convertResult = $convertCurrency->getResult();
$convertResult->next();
$current->result = $convertResult->getFloat(0);
}
} else {
$convertCurrency->addParameter(1, $amount);
$convertCurrency->addParameter(2, $from);
$convertCurrency->addParameter(3, $to);
$convertCurrency->execute();
$convertResult = $convertCurrency->getResult();
$convertResult->next();
$result->result = $convertResult->getFloat(0);
}
}
http_response_code(200);
header('Content-Type: application/json');
echo json_encode($result);

View file

@ -1,167 +0,0 @@
<?php
/*
* (21:42:47) malloc_CDLVII: i think that's one of the two rules of Flash Wave Development
* (21:42:57) malloc_CDLVII: 1. if it is written, it will be rewritten
* (21:43:04) malloc_CDLVII: 2. it will have automatic updates
*/
header('Content-Type: text/plain; charset=utf-8');
ini_set('display_errors', 'on');
error_reporting(-1);
if($_SERVER['REQUEST_METHOD'] !== 'POST')
die('no');
$config = __DIR__ . '/../config/git-broadcast.ini';
if(!is_file($config))
die('config missing');
$config = parse_ini_file($config, true);
if(empty($config['tokens']['token']))
die('config invalid');
$isGitea = isset($_SERVER['HTTP_X_GITEA_DELIVERY']) && isset($_SERVER['HTTP_X_GITEA_EVENT']);
$rawData = file_get_contents('php://input');
$sigParts = $isGitea
? ['sha256', $_SERVER['HTTP_X_GITEA_SIGNATURE']]
: explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '', 2);
if(empty($sigParts[1]))
die('invalid signature');
$repoAuthenticated = false;
foreach($config['tokens']['token'] as $repoName => $repoToken) {
if(hash_equals(hash_hmac($sigParts[0], $rawData, $repoToken), $sigParts[1])) {
$repoAuthenticated = true;
break;
}
}
if(!$repoAuthenticated)
die('signature check failed');
$data = json_decode($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'
? $_POST['payload']
: $rawData);
if(empty($data))
die('body is corrupt');
if(empty($data->repository->full_name))
die('body is corrupt');
if($data->repository->full_name !== $repoName)
die('invalid repository token');
$message = '';
switch($_SERVER['HTTP_X_GITHUB_EVENT']) {
case 'ping':
$message = "ping received from {$data->repository->full_name}!";
break;
case 'create':
switch ($data->ref_type) {
case 'tag':
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] created tag [url={$data->repository->html_url}/releases/tag/{$data->ref}]{$data->ref}[/url]";
break;
case 'branch':
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] created branch [url={$data->repository->html_url}/tree/{$data->ref}]{$data->ref}[/url]";
break;
case 'repository':
// doubt we'll ever get here
// TODO: test organisation level webhooks?
break;
}
break;
case 'delete':
switch($data->ref_type) {
case 'tag':
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted tag {$data->ref}";
break;
case 'branch':
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted branch {$data->ref}";
break;
case 'repository':
$message = "[b]{$data->repository->full_name}[/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted the repository :crying:";
break;
}
break;
case 'push':
$commitCount = count($data->commits);
if($commitCount < 1)
break;
$message .= "[b]{$data->repository->full_name}:" . substr($data->ref, strrpos($data->ref, '/') + 1) . "[/b] {$commitCount} new commit" . ($commitCount === 1 ? '' : 's') . "\r\n";
foreach($data->commits as $commit) {
$timestamp = strtotime($commit->timestamp);
$commit->message = trim($commit->message);
$message .= "[b][url={$commit->url}][code]" . substr($commit->id, 0, 7) . "[/code][/url][/b] {$commit->message} - [url=" . ($isGitea ? 'https://git.flash.moe/' : 'https://github.com/') . "{$commit->author->username}]{$commit->author->username}[/url]\r\n";
}
break;
case 'status':
$status_colour['failure'] = '#cb2431';
//$status_colour['pending'] = '#b5ab86';
$status_colour['success'] = '#28a745';
$status_colour['error'] = '#586069';
$status_emotes['failure'] = [':angry:', ':angrier:', ':angriest:', ':sad:', ':ouch:', ':dizzy:', ':sweat:', ':fat:', ':jew:'];
$status_emotes['success'] = [':happy:', ':lol:', ':yay:'];
$status_emotes['error'] = [':blank:'];
$status_name['continuous-integration/travis-ci/push'] = 'Unit Tests';
$status_name['continuous-integration/styleci/push'] = 'Style Check';
if (!array_key_exists($data->context, $status_name) || !array_key_exists($data->state, $status_colour)) {
//$message = "/msg flash {$data->context}: {$data->state}";
break;
}
$message = "[b][url={$data->commit->html_url}]{$data->repository->full_name}[/url] [url={$data->target_url}]" . $status_name[$data->context] . '[/url][/b]: [b][i][color=' . $status_colour[$data->state] . ']' . ucfirst($data->state) . ' ' . $status_emotes[$data->state][array_rand($status_emotes[$data->state])] . '[/color][/i][/b]';
break;
case 'watch':
switch($data->action) {
case 'started':
$message = "[url={$data->sender->html_url}]{$data->sender->login}[/url] starred [url={$data->repository->html_url}]{$data->repository->full_name}[/url] :love:";
break;
}
break;
case 'fork':
$message = "[url={$data->forkee->html_url}]{$data->forkee->owner->login}[/url] forked [url={$data->repository->html_url}]{$data->repository->full_name}[/url] :omg:";
break;
case 'issues':
if(!in_array($data->action, ['opened', /*'edited',*/ 'closed', 'reopened']))
break;
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url] [url={$data->sender->html_url}]{$data->sender->login}[/url] {$data->action} issue [/b][url={$data->issue->html_url}][b]#{$data->issue->number}[/b]: {$data->issue->title}[/url]";
break;
case 'pull_request':
if(!in_array($data->action, ['opened', /*'edited',*/ 'closed', 'reopened']))
break;
$message = "[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url]: [url={$data->sender->html_url}]{$data->sender->login}[/url] {$data->action} pull request [/b][url={$data->pull_request->html_url}][b]#{$data->pull_request->number}[/b]: {$data->pull_request->title}[/url]";
break;
}
if(!empty($message)) {
// the
var_dump($message);
$sock = fsockopen($config['boat']['host'], $config['boat']['port'], $errno, $errstr, 5);
$message = chr(1) . '/msg flash ' . $message;
$message = hash_hmac('sha256', $message, $config['boat']['secret'], true) . $message;
fwrite($sock, $message);
}

View file

@ -1,50 +1,21 @@
<?php
http_response_code(404);
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Cannot find page</title>
<link rel="stylesheet" type="text/css" href="/satori.css"/>
</head>
<body>
<div id="wrap">
<h1>
<img src="//static.flash.moe/images/404-info.gif"/>
The page cannot be found
</h1>
<p>
The page you are looking for might have been removed, had its
name changed, or is temporarily unavailable.
</p>
<hr>
<h2>
Please try the following:
</h2>
<ul>
<li>
If you typed the page address in the Address bar, make
sure that it is spelled correctly.
</li>
<li>
Open the <a href="//flashii.net">flashii.net</a>
home page, and then look for links to the information you want.
</li>
<li>
Click the <img src="//static.flash.moe/images/404-back.gif"/><a href="javascript:void(0);" onclick="history.go(-1);">Back</a>
button to try another link.
</li>
<li>
Click <img src="//static.flash.moe/images/404-search.gif"/><a href="//flashii.net/search.php">Search</a>
to look for information on the Internet.
</li>
</ul>
<h3>
HTTP 404 - File not found
<br/>
Internet Explorer
</h3>
</div>
</body>
</html>
namespace Satori;
require_once __DIR__ . '/../satori.php';
set_exception_handler(function(\Throwable $ex) {
http_response_code(500);
ob_clean();
if(SAT_DEBUG) {
header('Content-Type: text/plain; charset=utf-8');
echo (string)$ex;
exit;
}
header('Content-Type: text/html; charset=utf-8');
echo file_get_contents(SAT_DIR_PUBLIC . '/404.html');
exit;
});
$sat->createRouting()->dispatch();

File diff suppressed because it is too large Load diff

View file

@ -1,150 +0,0 @@
<?php
define('TL_FORMAT', strtolower($_GET['fmt'] ?? 'json'));
if (isset($_SERVER['HTTP_USER_AGENT'], $_GET['pretty']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Mozilla/') !== false)
define('JSON_ARGS', JSON_PRETTY_PRINT);
else
define('JSON_ARGS', 0);
function out($arr, $mode) {
$output = '';
$mimeType = 'text/plain';
switch (TL_FORMAT) {
case 'xml':
$mimeType = 'application/xml';
$output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
switch ($mode) {
case 'list':
$type = get_class($arr[0]);
$output .= "<ArrayOf{$type}>";
foreach ($arr as $part) {
$output .= "<{$type} ";
foreach (get_object_vars($part) as $name => $value) {
$output .= "{$name}=\"{$value}\" ";
}
$output .= '/>';
}
$output .= "</ArrayOf{$type}>";
break;
case 'do':
$output .= "<Translation ";
foreach ($arr as $name => $value) {
$value = htmlspecialchars($value);
$output .= "{$name}=\"{$value}\" ";
}
$output .= '/>';
break;
}
break;
case 'json':
default:
$mimeType = 'application/json';
$output = json_encode($arr, JSON_ARGS);
break;
}
header("Content-Type: {$mimeType}; charset=utf-8");
die($output);
}
define('TRANSLATE_URL', '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');
define('CURL_USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0');
define('MAX_TRANSL_LENGTH', 630);
function curl_req($url) {
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => CURL_USER_AGENT,
]);
return curl_exec($curl);
}
class Language {
public $code;
public $name;
public function __construct($code, $name) {
$this->code = $code;
$this->name = $name;
}
public static function fromCode($code) {
return new Language($code, locale_get_display_name($code));
}
}
$languages = [
'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',
];
$mode = $_GET['m'] ?? null;
switch ($mode) {
case 'list':
$out = [];
foreach ($languages as $lang)
$out[] = Language::fromCode($lang);
out($out, $mode);
case 'do':
$from = strtolower($_GET['f'] ?? 'auto');
$to = strtolower($_GET['t'] ?? 'en');
if ($from !== 'auto' && !in_array($from, $languages))
out(['error' => 'The given origin language is invalid.'], $mode);
if (!in_array($to, $languages))
out(['error' => 'The given destination language is invalid.'], $mode);
$text = isset($_GET['z']) ? $_GET['z'] : file_get_contents('php://input');
$t_len = strlen($text);
if ($t_len < 1)
out(['error' => 'Nothing to translate.'], $mode);
if ($t_len > MAX_TRANSL_LENGTH)
out(['error' => 'Too much text, please split some up.'], $mode);
$google_txt = curl_req(sprintf(TRANSLATE_URL, $from, $to, rawurlencode($text)));
$google = json_decode($google_txt);
if (strlen($google_txt) < 1 || !$google)
out(['error' => 'Something happened.'], $mode);
$result = $google[0][0][0];
$original = $google[0][0][1];
$from = $google[2];
$from_text = locale_get_display_name($from);
$to_text = locale_get_display_name($to);
out(compact('from', 'from_text', 'to', 'to_text', 'original', 'result'), $mode);
}

View file

@ -1,39 +0,0 @@
<?php
$config = parse_ini_file(__DIR__ . '/../config/flashii.ini');
header('Content-Type: application/json; charset=utf-8');
$word = (string)filter_input(INPUT_GET, 'word');
if(empty($word))
die('{"error":"word"}');
$response = json_decode(file_get_contents(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),
$config['wordnik-token']
)));
if(empty($response))
die('{"error":"response"}');
$defs = [];
foreach($response as $def) {
if(empty($def->text))
continue;
$defs[] = [
'word' => $def->word ?? '',
'pos' => $def->partOfSpeech ?? '',
'attr' => $def->attributionText ?? '',
'attr_url' => $def->attributionUrl ?? '',
'dict' => $def->sourceDictionary ?? '',
'text' => strtr($def->text, [
'<em>' => '[i]',
'</em>' => '[/i]',
]),
'url' => $def->wordnikUrl ?? '',
];
}
echo json_encode($defs);

25
satori.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace Satori;
use Index\Environment;
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', '20231029');
require_once SAT_ROOT . '/vendor/autoload.php';
Environment::setDebug(SAT_DEBUG);
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
$cfg = SharpConfig::fromFile(SAT_ROOT . '/config.cfg');
$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,
];
}
}

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

@ -0,0 +1,60 @@
<?php
namespace Satori\Booru;
use stdClass;
use Index\XArray;
use Index\Routing\Route;
use Index\Routing\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;
});
}
#[Route('GET', '/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,62 @@
<?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 {
$params = [];
if($tags !== '')
$params['tags'] = $tags;
if($limit > 0)
$params['limit'] = $limit;
if($random)
$params['random'] = '1';
$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 false; // remote embedding is blocked
}
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 false; // remote embedding is blocked
}
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,32 @@
<?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 {
return false; // remote embedding is blocked
}
public function getFileUrl(): string {
return $this->info['file_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));
}
}

30
src/DatabaseContext.php Normal file
View file

@ -0,0 +1,30 @@
<?php
namespace Satori;
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo;
use Index\Data\Migration\DbMigrationManager;
use Index\Data\Migration\FsDbMigrationRepo;
class DatabaseContext {
public function __construct(
private IDbConnection $connection
) {}
public function getConnection(): IDbConnection {
return $this->connection;
}
public function getQueryCount(): int {
$result = $this->connection->query('SHOW SESSION STATUS LIKE "Questions"');
return $result->next() ? $result->getInteger(1) : 0;
}
public function createMigrationManager(): DbMigrationManager {
return new DbMigrationManager($this->connection);
}
public function createMigrationRepo(): IDbMigrationRepo {
return new FsDbMigrationRepo(SAT_DIR_MIGRATIONS);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Satori\Dictionary;
use Syokuhou\IConfig;
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 = json_decode(file_get_contents(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')
)), true);
if(empty($data))
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,65 @@
<?php
namespace Satori\Dictionary;
use Index\Routing\Route;
use Index\Routing\RouteHandler;
class DictionaryRoutes extends RouteHandler {
public function __construct(
private DictionaryContext $context
) {}
#[Route('GET', '/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;
}
#[Route('GET', '/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,68 @@
<?php
namespace Satori\ExRate;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
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 = json_decode(file_get_contents(sprintf(
'http://api.exchangerate.host/live?source=%s&access_key=%s',
self::BASE_CURRENCY,
$this->config->getString('token')
)), true);
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,93 @@
<?php
namespace Satori\ExRate;
use stdClass;
use Index\Routing\Route;
use Index\Routing\RouteHandler;
class ExRateRoutes extends RouteHandler {
public function __construct(
private ExRateContext $context
) {}
#[Route('GET', '/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;
}
#[Route('GET', '/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;
}
}

36
src/RoutingContext.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace Satori;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
use Index\Routing\IRouter;
use Index\Routing\IRouteHandler;
class RoutingContext {
private HttpFx $router;
public function __construct() {
$this->router = new HttpFx;
$this->router->use('/', fn($resp) => $resp->setPoweredBy('Satori'));
}
public function getRouter(): IRouter {
return $this->router;
}
public function registerDefaultErrorPages(): void {
$this->router->addErrorHandler(401, fn($resp) => $resp->setContent(file_get_contents(SAT_DIR_PUBLIC . '/404.html')));
$this->router->addErrorHandler(403, fn($resp) => $resp->setContent(file_get_contents(SAT_DIR_PUBLIC . '/404.html')));
$this->router->addErrorHandler(404, fn($resp) => $resp->setContent(file_get_contents(SAT_DIR_PUBLIC . '/404.html')));
$this->router->addErrorHandler(500, fn($resp) => $resp->setContent(file_get_contents(SAT_DIR_PUBLIC . '/404.html')));
$this->router->addErrorHandler(503, fn($resp) => $resp->setContent(file_get_contents(SAT_DIR_PUBLIC . '/404.html')));
}
public function register(IRouteHandler $handler): void {
$this->router->register($handler);
}
public function dispatch(?HttpRequest $request = null): void {
$this->router->dispatch($request);
}
}

23
src/SFileCache.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace Satori;
final class SFileCache {
public static function filePath(string $name): string {
return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'satori-' . hash('xxh3', $name) . '.tmp';
}
public static function retrieve(string $name, callable $match, callable $create): mixed {
$fileName = self::filePath($name);
if(is_file($fileName) && $match(filemtime($fileName), $fileName))
return unserialize(file_get_contents($fileName));
$output = $create();
file_put_contents($fileName, serialize($output));
return $output;
}
public static function for15mins(string $name, callable $create): mixed {
return self::retrieve($name, fn($mtime) => is_int($mtime) && floor($mtime / 900) >= floor(time() / 900), $create);
}
}

32
src/SHttp.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace Satori;
final class SHttp {
public static function getJson(string $url): mixed {
$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 $response;
}
public static function getJsonCached(string $url): mixed {
return SFileCache::for15mins($url, fn() => self::getJson($url));
}
}

67
src/SatoriContext.php Normal file
View file

@ -0,0 +1,67 @@
<?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->registerDefaultErrorPages();
$routingCtx->register(new ExRate\ExRateRoutes($this->createExRates()));
$routingCtx->register(new Dictionary\DictionaryRoutes($this->createDictionary()));
$routingCtx->register(new Translation\TranslateRoutes($this->createTranslation()));
$routingCtx->register(new Booru\BooruRoutes($this->createBooru()));
$routingCtx->register(new Splatoon\SplatoonRoutes($this->createSplatoon()));
return $routingCtx;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonFestival {
public function getId(): string;
public function getLocalId(): string;
public function getTitle(): string;
public function getImage(): string;
public function getRegion(): string;
public function getStartTime(): int;
public function getEndTime(): int;
public function getState(): string;
public function canVote(): bool;
public function getTeams(): array;
public function hasSpecialStages(): bool;
public function getSpecialStages(): array;
}

View file

@ -0,0 +1,14 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonFestivalTeam {
public function getId(): string;
public function getLocalId(): string;
public function getName(): string;
public function getImage(): string;
public function getColour(): int;
public function hasResults(): bool;
public function isWinner(): bool;
public function getResults(): array;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonFestivalTeamResult {
public function getCategory(): string;
public function getRatio(): float;
public function isTop(): bool;
}

View file

@ -0,0 +1,14 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonGame {
public function getName(): string;
public function getTitle(): string;
public function getColour(): int;
public function getDefaultLocale(): string;
public function isSupportedLocale(string $locale): bool;
public function getLocales(): array;
public function getLocaleAlias(string $locale): string;
public function createLocale(string $locale): ISplatoonLocale;
}

View file

@ -0,0 +1,10 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonGameMode {
public function getName(): string;
public function getTitle(): string;
public function getColour(): int;
public function hasVariants(): bool;
public function getVariants(): array;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonGameModeVariant {
public function getName(): string;
public function getTitle(): string;
public function getColour(): int;
}

View file

@ -0,0 +1,9 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonHasFestivals extends ISplatoonGame {
public function isSupportedFestivalRegion(string $region): bool;
public function getFestivalRegions(): array;
public function getFestivalRegionAlias(string $region): string;
public function getFestivals(ISplatoonLocale $locale, array $regions = []): array;
}

View file

@ -0,0 +1,11 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonHasSchedules extends ISplatoonGame {
public function isValidScheduleFilter(string $filter): bool;
public function getScheduleFestival(): ?ISplatoonScheduleFestival;
public function getScheduleFilters(): array;
public function getScheduleFilterAlias(string $filter): string;
public function getScheduleModes(ISplatoonLocale $locale, array $filters): array;
public function getSchedules(ISplatoonLocale $locale, array $filters): array;
}

View file

@ -0,0 +1,10 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonLeague {
public function getId(): string;
public function getName(): string;
public function getDescription(): string;
public function getRulesLines(): array;
public function getRulesUrl(): string;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonLocale {
public function getString(string $name, string $fallback): string;
}

View file

@ -0,0 +1,9 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleEntry {
public function getGameMode(): string;
public function getGameModeVariant(): string;
public function getTimePeriods(): array;
public function getStages(): array;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleEntryCoop extends ISplatoonScheduleEntry {
public function getWeapons(): array;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleEntryVs extends ISplatoonScheduleEntry {
public function getRuleset(): ISplatoonVsRuleset;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleEntryVsLeague extends ISplatoonScheduleEntryVs {
public function getLeagueInfo(): ISplatoonLeague;
}

View file

@ -0,0 +1,14 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleFestival {
public function getId(): string;
public function getTitle(): string;
public function getStartTime(): int;
public function getMidTermTime(): int;
public function getEndTime(): int;
public function getState(): string;
public function getTeams(): array;
}

View file

@ -0,0 +1,7 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonScheduleFestivalTeam {
public function getId(): string;
public function getColour(): int;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonStage {
public function getId(): string;
public function getName(): string;
public function getImage(): string;
}

View file

@ -0,0 +1,7 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonTimePeriod {
public function getStartTime(): int;
public function getEndTime(): int;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonVsRuleset {
public function getId(): string;
public function getName(): string;
public function getShortName(): string;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Satori\Splatoon;
interface ISplatoonWeapon {
public function getId(): string;
public function getName(): string;
public function getImage(): string;
}

View file

@ -0,0 +1,26 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonStage;
class Splatoon2CoopStage implements ISplatoonStage {
public function __construct(
private Splatoon2Locale $locale,
private array $stageInfo
) {}
public function getId(): string {
return $this->stageInfo['image'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('stages:%s:name', $this->stageInfo['image']),
$this->stageInfo['name']
);
}
public function getImage(): string {
return Splatoon2Game::asset($this->stageInfo['image']);
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonFestival;
class Splatoon2Festival implements ISplatoonFestival {
public function __construct(
private Splatoon2Locale $locale,
private string $region,
private array $festInfo,
private array $results
) {}
public function getId(): string {
return (string)$this->festInfo['festival_id'];
}
public function getLocalId(): string {
return base64_encode(sprintf('Fest-%s:JEA-%05d', $this->region, $this->festInfo['festival_id']));
}
public function getTitle(): string {
$prefix = sprintf('festivals:%s:', $this->festInfo['festival_id']);
return sprintf(
'%s vs %s',
$this->locale->getString($prefix . 'alpha_short', 'ALPHA'),
$this->locale->getString($prefix . 'bravo_short', 'BRAVO')
);
}
public function getImage(): string {
return Splatoon2Game::asset($this->festInfo['images']['panel']);
}
public function getRegion(): string {
return $this->region;
}
public function getStartTime(): int {
return $this->festInfo['times']['start'];
}
public function getEndTime(): int {
return $this->festInfo['times']['end'];
}
public function getState(): string {
$time = time();
if($time > $this->getEndTime())
return 'CLOSED';
if($time > $this->getStartTime())
return 'OPEN'; // this is an assumption
return 'SCHEDULED';
}
public function canVote(): bool {
return $this->getEndTime() > time();
}
public function getTeams(): array {
$teams = [];
$tmpResults = [];
foreach(['alpha', 'bravo'] as $teamNo => $teamId) {
$rawColour = $this->festInfo['colors'][$teamId];
$teamColour = (round($rawColour['a'] * 0xFF) << 24)
| (round($rawColour['r'] * 0xFF) << 16)
| (round($rawColour['g'] * 0xFF) << 8)
| round($rawColour['b'] * 0xFF);
$results = [];
$isWinner = false;
// god i hope this works
if(!empty($this->results)) {
$isWinner = $this->results['summary']['total'] == $teamNo;
foreach(['challenge', 'vote', 'regular', 'solo', 'team'] as $rateType) {
if(!array_key_exists($rateType, $this->results['rates']))
continue;
$rate = $this->results['rates'][$rateType][$teamId];
if(!isset($tmpResults[$rateType]))
$tmpResults[$rateType] = $rate > $this->results['rates'][$rateType]['bravo'];
$results[] = new Splatoon2FestivalTeamResult(
$rateType,
$rate / 10000,
$tmpResults[$rateType],
);
$tmpResults[$rateType] = !$tmpResults[$rateType];
}
}
$teams[] = new Splatoon2FestivalTeam(
$this->locale,
$this->region,
$this->festInfo['festival_id'],
$teamId,
$this->festInfo['names'][$teamId . '_long'],
$this->festInfo['images'][$teamId],
$teamColour,
$isWinner,
$results
);
}
return $teams;
}
public function hasSpecialStages(): bool {
return array_key_exists('special_stage', $this->festInfo);
}
public function getSpecialStages(): array {
return [new Splatoon2Stage($this->locale, $this->festInfo['special_stage'])];
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonFestivalTeam;
class Splatoon2FestivalTeam implements ISplatoonFestivalTeam {
public function __construct(
private Splatoon2Locale $locale,
private string $region,
private string $festId,
private string $teamId,
private string $teamName,
private string $teamImage,
private int $teamColour,
private bool $isWinner,
private array $results
) {}
public function getId(): string {
return $this->teamId;
}
public function getLocalId(): string {
return base64_encode(sprintf(
'FestTeam-%s:JEA-%05d:%s',
$this->region,
$this->festId,
ucfirst($this->teamId)
));
}
public function getName(): string {
return $this->locale->getString(
sprintf('festivals:%s:names:%s_long', $this->festId, $this->teamId),
$this->teamName
);
}
public function getImage(): string {
return Splatoon2Game::asset($this->teamImage);
}
public function getColour(): int {
return $this->teamColour;
}
public function hasResults(): bool {
return !empty($this->results);
}
public function isWinner(): bool {
return $this->isWinner;
}
public function getResults(): array {
return $this->results;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonFestivalTeamResult;
class Splatoon2FestivalTeamResult implements ISplatoonFestivalTeamResult {
public function __construct(
private string $category,
private float $ratio,
private bool $isTop
) {}
public function getCategory(): string {
return $this->category;
}
public function getRatio(): float {
return $this->ratio;
}
public function isTop(): bool {
return $this->isTop;
}
}

View file

@ -0,0 +1,243 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\SHttp;
use Satori\Splatoon\ISplatoonScheduleFestival;
use Satori\Splatoon\ISplatoonGame;
use Satori\Splatoon\ISplatoonHasFestivals;
use Satori\Splatoon\ISplatoonHasSchedules;
use Satori\Splatoon\ISplatoonLocale;
class Splatoon2Game implements ISplatoonGame, ISplatoonHasFestivals, ISplatoonHasSchedules {
private const URL = 'https://splatoon2.ink';
private const ASSET_URL = self::URL . '/assets/splatnet%s';
public static function asset(string $path): string {
return sprintf(self::ASSET_URL, $path);
}
public function getName(): string {
return 's2';
}
public function getTitle(): string {
return 'Splatoon 2';
}
public function getColour(): int {
return 0xFFFF4506;
}
private const LOCALE_URL = self::URL . '/data/locale/%s.json';
private const DEFAULT_LOCALE = 'en';
private const LOCALES = ['en', 'es', 'es-MX', 'fr', 'fr-CA', 'de', 'nl', 'it', 'ru', 'ja'];
private const LOCALE_ALIASES = [
'en-US' => 'en', 'en-GB' => 'en',
'es-ES' => 'es', 'es-MX' => 'es',
'fr-FR' => 'fr', 'fr-CA' => 'fr',
'de-DE' => 'de', 'nl-NL' => 'nl',
'it-IT' => 'it', 'ru-RU' => 'ru',
'ja-JP' => 'ja',
];
public function getDefaultLocale(): string {
return self::DEFAULT_LOCALE;
}
public function isSupportedLocale(string $locale): bool {
return in_array($locale, self::LOCALES);
}
public function getLocales(): array {
return self::LOCALES;
}
public function getLocaleAlias(string $locale): string {
if($locale === '')
return self::DEFAULT_LOCALE;
if(array_key_exists($locale, self::LOCALE_ALIASES))
return self::LOCALE_ALIASES[$locale];
return $locale;
}
public function createLocale(string $locale): ISplatoonLocale {
return new Splatoon2Locale(
SHttp::getJsonCached(sprintf(self::LOCALE_URL, $locale))
);
}
private const FEST_URL = self::URL . '/data/festivals.json';
private const FEST_REGIONS = ['eu', 'jp', 'na'];
private const FEST_REGIONS_ALIAS = [
'EU' => 'eu', 'JP' => 'jp', 'US' => 'na',
];
public function isSupportedFestivalRegion(string $region): bool {
return in_array($region, self::FEST_REGIONS);
}
public function getFestivalRegions(): array {
return self::FEST_REGIONS;
}
public function getFestivalRegionAlias(string $region): string {
if(array_key_exists($region, self::FEST_REGIONS_ALIAS))
return self::FEST_REGIONS_ALIAS[$region];
return $region;
}
public function getFestivals(ISplatoonLocale $locale, array $regions = []): array {
if(empty($regions))
$regions = self::FEST_REGIONS;
$raw = SHttp::getJsonCached(self::FEST_URL);
$fests = [];
$regionsReverse = array_flip(self::FEST_REGIONS_ALIAS);
foreach($regions as $region) {
if(!array_key_exists($region, $raw) || !array_key_exists('festivals', $raw[$region]))
continue;
$festivals = $raw[$region]['festivals'];
$resultsRaw = $raw[$region]['results'];
$results = [];
if(array_key_exists($region, $regionsReverse))
$region = $regionsReverse[$region];
foreach($resultsRaw as $result)
$results[$result['festival_id']] = $result;
foreach($festivals as $festId => $festRaw)
$fests[] = new Splatoon2Festival($locale, $region, $festRaw, $results[$festRaw['festival_id']]);
}
return $fests;
}
private const SCHED_URL = self::URL . '/data/schedules.json';
private const SCHED_COOP_URL = self::URL . '/data/coop-schedules.json';
private const SCHED_FILTERS = [
'regular' => 'regular',
'turf' => 'regular',
'series' => 'gachi',
'open' => 'gachi',
'ranked' => 'gachi',
'bankara' => 'gachi',
'gachi' => 'gachi',
'x' => 'gachi',
'challenge' => 'league',
'league' => 'league',
'salmon' => 'coop',
'coop' => 'coop',
'fest' => 'fest',
'splatfest' => 'fest',
'festival' => 'fest',
];
public function isValidScheduleFilter(string $filter): bool {
return array_key_exists($filter, self::SCHED_FILTERS);
}
public function getScheduleFilters(): array {
return array_unique(array_values(self::SCHED_FILTERS));
}
public function getScheduleFilterAlias(string $filter): string {
if(array_key_exists($filter, self::SCHED_FILTERS))
return self::SCHED_FILTERS[$filter];
return $filter;
}
private ?array $rawSchedule = null;
private function getRawScheduleData(): array {
if($this->rawSchedule === null)
$this->rawSchedule = SHttp::getJsonCached(self::SCHED_URL);
return $this->rawSchedule;
}
private ?array $rawCoopSchedule = null;
private function getRawCoopScheduleData(): array {
if($this->rawCoopSchedule === null)
$this->rawCoopSchedule = SHttp::getJsonCached(self::SCHED_COOP_URL);
return $this->rawCoopSchedule;
}
public function getScheduleModes(ISplatoonLocale $locale, array $filters): array {
$modes = [];
$includeRegular = in_array('regular', $filters);
$includeGachi = in_array('gachi', $filters);
$includeLeague = in_array('league', $filters);
$includeCoop = in_array('coop', $filters);
if($includeRegular || $includeGachi || $includeLeague) {
$regularSchedule = $this->getRawScheduleData();
if($includeRegular && !empty($regularSchedule['regular']))
$modes[] = new Splatoon2GameMode($locale, 'regular', 'Regular Battle', 0xFF19D719);
if($includeGachi && !empty($regularSchedule['gachi']))
$modes[] = new Splatoon2GameMode($locale, 'gachi', 'Ranked Battle', 0xFFF54910);
if($includeLeague && !empty($regularSchedule['league']))
$modes[] = new Splatoon2GameMode($locale, 'league', 'League Battle', 0xFFF02D7D);
}
if($includeCoop) {
$coopSchedule = $this->getRawCoopScheduleData();
if(!empty($coopSchedule['details']))
$modes[] = new Splatoon2GameMode($locale, 'coop', 'Salmon Run', 0xFFFF5600);
}
return $modes;
}
public function getScheduleFestival(): ?ISplatoonScheduleFestival {
return null;
}
public function getSchedules(ISplatoonLocale $locale, array $filters): array {
$schedules = [];
$includeRegular = in_array('regular', $filters);
$includeGachi = in_array('gachi', $filters);
$includeLeague = in_array('league', $filters);
$includeCoop = in_array('coop', $filters);
if($includeRegular || $includeGachi || $includeLeague) {
$regularSchedule = $this->getRawScheduleData();
if($includeRegular && !empty($regularSchedule['regular']))
foreach($regularSchedule['regular'] as $entryInfo)
$schedules[] = new Splatoon2ScheduleEntryVs($locale, $entryInfo);
if($includeGachi && !empty($regularSchedule['gachi']))
foreach($regularSchedule['gachi'] as $entryInfo)
$schedules[] = new Splatoon2ScheduleEntryVs($locale, $entryInfo);
if($includeLeague && !empty($regularSchedule['league']))
foreach($regularSchedule['league'] as $entryInfo)
$schedules[] = new Splatoon2ScheduleEntryVs($locale, $entryInfo);
}
if($includeCoop) {
$coopSchedule = $this->getRawCoopScheduleData();
if(!empty($coopSchedule['schedules']))
foreach($coopSchedule['schedules'] as $key => $schedule) {
if(array_key_exists($key, $coopSchedule['details']))
$schedule = $coopSchedule['details'][$key];
$schedules[] = new Splatoon2ScheduleEntryCoop($locale, $schedule);
}
}
return $schedules;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonGameMode;
class Splatoon2GameMode implements ISplatoonGameMode {
public function __construct(
private Splatoon2Locale $locale,
private string $name,
private string $title,
private int $colour
) {}
public function getName(): string {
return $this->name;
}
public function getTitle(): string {
return $this->locale->getString(
sprintf('game_modes:%s:name', $this->name),
$this->title
);
}
public function getColour(): int {
return $this->colour;
}
public function hasVariants(): bool {
return false;
}
public function getVariants(): array {
return [];
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonLocale;
class Splatoon2Locale implements ISplatoonLocale {
public function __construct(
private array $strings
) {}
public function getString(string $name, string $fallback): string {
$name = explode(':', $name);
$nameCount = count($name);
if($nameCount > 1 && array_key_exists($name[0], $this->strings) && array_key_exists($name[1], $this->strings[$name[0]])) {
$stringInfo = $this->strings[$name[0]][$name[1]];
if($nameCount > 2) {
if(array_key_exists($name[2], $stringInfo)) {
if(is_array($stringInfo[$name[2]]) && $nameCount > 3) {
if(array_key_exists($name[3], $stringInfo[$name[2]]))
return $stringInfo[$name[2]][$name[3]];
return $fallback;
}
return $stringInfo[$name[2]];
}
if(array_key_exists('names', $stringInfo) && array_key_exists($name[2], $stringInfo['names']))
return $stringInfo['names'][$name[2]];
}
if(array_key_exists('name', $stringInfo))
return $stringInfo['name'];
if(!empty($stringInfo))
return $stringInfo[array_key_first($stringInfo)];
}
return $fallback;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Index\XArray;
use Satori\Splatoon\ISplatoonScheduleEntryCoop;
class Splatoon2ScheduleEntryCoop implements ISplatoonScheduleEntryCoop {
public function __construct(
private Splatoon2Locale $locale,
private array $entryInfo
) {}
public function getGameMode(): string {
return 'coop';
}
public function getGameModeVariant(): string {
return '';
}
public function getTimePeriods(): array {
return [
new Splatoon2TimePeriod($this->entryInfo['start_time'], $this->entryInfo['end_time']),
];
}
public function getStages(): array {
if(!array_key_exists('stage', $this->entryInfo))
return [];
return [
new Splatoon2CoopStage($this->locale, $this->entryInfo['stage']),
];
}
public function getWeapons(): array {
if(!array_key_exists('weapons', $this->entryInfo))
return [];
return XArray::select($this->entryInfo['weapons'], fn($weapon) => new Splatoon2Weapon($this->locale, $weapon));
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonScheduleEntryVs;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon2ScheduleEntryVs implements ISplatoonScheduleEntryVs {
public function __construct(
private Splatoon2Locale $locale,
private array $entryInfo
) {}
public function getGameMode(): string {
return $this->entryInfo['game_mode']['key'];
}
public function getGameModeVariant(): string {
return '';
}
public function getTimePeriods(): array {
return [
new Splatoon2TimePeriod($this->entryInfo['start_time'], $this->entryInfo['end_time']),
];
}
public function getStages(): array {
$stages = [];
foreach(['stage_a', 'stage_b', 'stage_c'] as $stage) {
if(!array_key_exists($stage, $this->entryInfo))
continue;
$stages[] = new Splatoon2Stage($this->locale, $this->entryInfo[$stage]);
}
return $stages;
}
public function getRuleset(): ISplatoonVsRuleset {
return new Splatoon2VsRuleset($this->locale, $this->entryInfo['rule']);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonStage;
class Splatoon2Stage implements ISplatoonStage {
public function __construct(
private Splatoon2Locale $locale,
private array $stageInfo
) {}
public function getId(): string {
return $this->stageInfo['id'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('stages:%s:name', $this->stageInfo['id']),
$this->stageInfo['name']
);
}
public function getImage(): string {
return Splatoon2Game::asset($this->stageInfo['image']);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonTimePeriod;
class Splatoon2TimePeriod implements ISplatoonTimePeriod {
public function __construct(
private int $startTime,
private int $endTime
) {}
public function getStartTime(): int {
return $this->startTime;
}
public function getEndTime(): int {
return $this->endTime;
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon2VsRuleset implements ISplatoonVsRuleset {
public function __construct(
private Splatoon2Locale $locale,
private array $rulesetInfo
) {}
public function getId(): string {
$id = $this->rulesetInfo['key'];
if($id === 'tower_control')
return 'LOFT';
if($id === 'splat_zones')
return 'AREA';
if($id === 'clam_blitz')
return 'CLAM';
if($id === 'rainmaker')
return 'GOAL';
if($id === 'turf_war')
return 'TURF_WAR';
return $id;
}
public function getName(): string {
return $this->locale->getString(
sprintf('rules:%s:name', $this->rulesetInfo['key']),
$this->rulesetInfo['name']
);
}
public function getShortName(): string {
$id = $this->rulesetInfo['key'];
if($id === 'tower_control')
return 'TC';
if($id === 'splat_zones')
return 'SZ';
if($id === 'clam_blitz')
return 'CB';
if($id === 'rainmaker')
return 'RM';
if($id === 'turf_war')
return 'TW';
return $id;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Satori\Splatoon\Splatoon2;
use Satori\Splatoon\ISplatoonWeapon;
class Splatoon2Weapon implements ISplatoonWeapon {
public function __construct(
private Splatoon2Locale $locale,
private array $weaponInfo
) {}
public function getId(): string {
return $this->weaponInfo['id'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('weapons:%s:name', $this->weaponInfo['id']),
$this->weaponInfo['weapon']['name']
);
}
public function getImage(): string {
return $this->weaponInfo['weapon']['image'];
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Index\XArray;
use Satori\Splatoon\ISplatoonFestival;
class Splatoon3Festival implements ISplatoonFestival {
public function __construct(
private Splatoon3Locale $locale,
private string $region,
private array $festInfo
) {}
public function getId(): string {
return $this->festInfo['__splatoon3ink_id'];
}
public function getLocalId(): string {
return $this->festInfo['id'];
}
public function getTitle(): string {
return $this->locale->getString(
sprintf('festivals:%s:title', $this->festInfo['__splatoon3ink_id']),
$this->festInfo['title']
);
}
public function getImage(): string {
return $this->festInfo['image']['url'];
}
public function getRegion(): string {
return $this->region;
}
public function getStartTime(): int {
return strtotime($this->festInfo['startTime']);
}
public function getEndTime(): int {
return strtotime($this->festInfo['endTime']);
}
public function getState(): string {
return $this->festInfo['state'];
}
public function canVote(): bool {
return $this->festInfo['isVotable'];
}
public function getTeams(): array {
$teamNo = -1;
$teams = [];
foreach($this->festInfo['teams'] as $teamInfo)
$teams[] = new Splatoon3FestivalTeam(
$this->locale,
$this->region,
$this->festInfo['__splatoon3ink_id'],
++$teamNo,
$teamInfo
);
return $teams;
}
public function hasSpecialStages(): bool {
return false;
}
public function getSpecialStages(): array {
return [];
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonFestivalTeam;
class Splatoon3FestivalTeam implements ISplatoonFestivalTeam {
public function __construct(
private Splatoon3Locale $locale,
private string $region,
private string $festId,
private int $teamNo,
private array $teamInfo
) {}
public function getId(): string {
return $this->teamInfo['id'];
}
public function getLocalId(): string {
return $this->teamInfo['id'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('festivals:%s:teams:%s', $this->festId, $this->teamNo),
$this->teamInfo['teamName']
);
}
public function getImage(): string {
return $this->teamInfo['image']['url'];
}
public function getColour(): int {
$colourInfo = $this->teamInfo['color'];
return (round($colourInfo['a'] * 0xFF) << 24)
| (round($colourInfo['r'] * 0xFF) << 16)
| (round($colourInfo['g'] * 0xFF) << 8)
| round($colourInfo['b'] * 0xFF);
}
public function hasResults(): bool {
return array_key_exists('result', $this->teamInfo);
}
public function isWinner(): bool {
return $this->teamInfo['result']['isWinner'];
}
public function getResults(): array {
$results = [];
foreach(['horagai', 'vote', 'regularContribution', 'challengeContribution', 'tricolorContribution'] as $rateType) {
$rateName = $rateType . 'Ratio';
if(!array_key_exists($rateName, $this->teamInfo['result'])
|| $this->teamInfo['result'][$rateName] === null)
continue;
if(str_ends_with($rateType, 'Contribution'))
$rateType = substr($rateType, 0, -12);
$results[] = new Splatoon3FestivalTeamResult(
$rateType,
$this->teamInfo['result'][$rateName],
$this->teamInfo['result']['is' . ucfirst($rateName) . 'Top']
);
}
return $results;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonFestivalTeamResult;
class Splatoon3FestivalTeamResult implements ISplatoonFestivalTeamResult {
public function __construct(
private string $category,
private float $ratio,
private bool $isTop
) {}
public function getCategory(): string {
return $this->category;
}
public function getRatio(): float {
return $this->ratio;
}
public function isTop(): bool {
return $this->isTop;
}
}

View file

@ -0,0 +1,342 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\SHttp;
use Satori\Splatoon\ISplatoonScheduleFestival;
use Satori\Splatoon\ISplatoonGame;
use Satori\Splatoon\ISplatoonHasFestivals;
use Satori\Splatoon\ISplatoonHasSchedules;
use Satori\Splatoon\ISplatoonLocale;
class Splatoon3Game implements ISplatoonGame, ISplatoonHasFestivals, ISplatoonHasSchedules {
private const URL = 'https://splatoon3.ink';
public function getName(): string {
return 's3';
}
public function getTitle(): string {
return 'Splatoon 3';
}
public function getColour(): int {
return 0xFFEFFE65;
}
private const LOCALE_URL = self::URL . '/data/locale/%s.json';
private const DEFAULT_LOCALE = 'en-US';
private const LOCALES = ['de-DE', 'en-US', 'en-GB', 'es-ES', 'es-MX', 'fr-FR', 'fr-CA', 'it-IT', 'ja-JP', 'ko-KR', 'nl-NL', 'ru-RU', 'zh-CN', 'zh-TW'];
public function getDefaultLocale(): string {
return self::DEFAULT_LOCALE;
}
public function isSupportedLocale(string $locale): bool {
return in_array($locale, self::LOCALES);
}
public function getLocales(): array {
return self::LOCALES;
}
public function getLocaleAlias(string $locale): string {
if($locale === '')
return self::DEFAULT_LOCALE;
return $locale;
}
public function createLocale(string $locale): ISplatoonLocale {
return new Splatoon3Locale(
SHttp::getJsonCached(sprintf(self::LOCALE_URL, $locale))
);
}
private const FEST_URL = self::URL . '/data/festivals.json';
private const FEST_REGIONS = ['US', 'EU', 'JP', 'AP'];
public function isSupportedFestivalRegion(string $region): bool {
return in_array($region, self::FEST_REGIONS);
}
public function getFestivalRegions(): array {
return self::FEST_REGIONS;
}
public function getFestivalRegionAlias(string $region): string {
return $region;
}
public function getFestivals(ISplatoonLocale $locale, array $regions = []): array {
if(empty($regions))
$regions = self::FEST_REGIONS;
$raw = SHttp::getJsonCached(self::FEST_URL);
$fests = [];
foreach($regions as $region) {
if(!array_key_exists($region, $raw)
|| !array_key_exists('data', $raw[$region])
|| !array_key_exists('festRecords', $raw[$region]['data'])
|| !array_key_exists('nodes', $raw[$region]['data']['festRecords']))
continue;
foreach($raw[$region]['data']['festRecords']['nodes'] as $festival)
$fests[] = new Splatoon3Festival($locale, $region, $festival);
}
return $fests;
}
private const SCHED_URL = self::URL . '/data/schedules.json';
private const SCHED_FILTERS = [
'regular' => 'regular',
'turf' => 'regular',
'series' => 'series',
'open' => 'open',
'ranked' => 'bankara',
'bankara' => 'bankara',
'gachi' => 'bankara',
'x' => 'x',
'challenge' => 'league',
'league' => 'league',
'salmon' => 'coop',
'coop' => 'coop',
'eggstra' => 'eggstra',
'bigrun' => 'bigrun',
'fest' => 'fest',
'splatfest' => 'fest',
'festival' => 'fest',
];
public function isValidScheduleFilter(string $filter): bool {
return array_key_exists($filter, self::SCHED_FILTERS);
}
public function getScheduleFilters(): array {
return array_unique(array_values(self::SCHED_FILTERS));
}
public function getScheduleFilterAlias(string $filter): string {
if(array_key_exists($filter, self::SCHED_FILTERS))
return self::SCHED_FILTERS[$filter];
return $filter;
}
private ?array $rawSchedule = null;
private function getRawScheduleData(): array {
if($this->rawSchedule === null)
$this->rawSchedule = SHttp::getJsonCached(self::SCHED_URL);
return $this->rawSchedule;
}
public function getScheduleModes(ISplatoonLocale $locale, array $filters): array {
$raw = $this->getRawScheduleData();
if(!array_key_exists('data', $raw))
return [];
$raw = $raw['data'];
$modes = [];
if(in_array('regular', $filters) && array_key_exists('regularSchedules', $raw) && !empty($raw['regularSchedules']['nodes']))
foreach($raw['regularSchedules']['nodes'] as $info) {
if(!empty($info['regularMatchSetting'])) {
$modes[] = new Splatoon3GameMode('regular', 'Regular Battle', 0xFF19D719);
break;
}
}
$includeRankedSeries = $includeRankedOpen = $includeRanked = in_array('bankara', $filters);
if(!$includeRanked) {
$includeRankedSeries = in_array('series', $filters);
$includeRankedOpen = in_array('open', $filters);
$includeRanked = $includeRankedSeries || $includeRankedOpen;
}
if($includeRanked && array_key_exists('bankaraSchedules', $raw) && !empty($raw['bankaraSchedules']['nodes'])) {
$variants = [];
foreach($raw['bankaraSchedules']['nodes'] as $info) {
if(empty($info['bankaraMatchSettings']))
continue;
if($includeRankedSeries && !empty($info['bankaraMatchSettings'][0])) {
$includeRankedSeries = false;
$variants[] = new Splatoon3GameModeVariant('CHALLENGE', 'Series', 0xFF603BFF);
}
if($includeRankedOpen && !empty($info['bankaraMatchSettings'][1])) {
$includeRankedOpen = false;
$variants[] = new Splatoon3GameModeVariant('OPEN', 'Open', 0xFF603BFF);
}
if(!$includeRankedSeries && !$includeRankedOpen)
break;
}
if(!empty($variants))
$modes[] = new Splatoon3GameMode('bankara', 'Anarchy Battle', 0xFFF54910, $variants);
}
if(in_array('x', $filters) && array_key_exists('xSchedules', $raw) && !empty($raw['xSchedules']['nodes']))
foreach($raw['xSchedules']['nodes'] as $info) {
if(!empty($info['xMatchSetting'])) {
$modes[] = new Splatoon3GameMode('x', 'X Battle', 0xFF0FDB9B);
break;
}
}
if(in_array('league', $filters) && array_key_exists('eventSchedules', $raw) && !empty($raw['eventSchedules']['nodes']))
foreach($raw['eventSchedules']['nodes'] as $info) {
if(!empty($info['leagueMatchSetting'])) {
$modes[] = new Splatoon3GameMode('league', 'Challenge Battle', 0xFFF02D7D);
break;
}
}
if(in_array('fest', $filters) && array_key_exists('festSchedules', $raw) && !empty($raw['festSchedules']['nodes'])) {
foreach($raw['festSchedules']['nodes'] as $info) {
if(!empty($info['festMatchSettings'])) {
$modes[] = new Splatoon3GameMode('fest', 'Splatfest Battle', 0xFF71717A);
break;
}
}
if(!empty($raw['currentFest']['tricolorStage']))
$modes[] = new Splatoon3GameMode('tricolor', 'Tricolor Battle', 0xFF71717A);
}
if(array_key_exists('coopGroupingSchedule', $raw)) {
$coop = $raw['coopGroupingSchedule'];
if(in_array('coop', $filters) && array_key_exists('regularSchedules', $coop) && !empty($coop['regularSchedules']['nodes']))
foreach($coop['regularSchedules']['nodes'] as $info) {
if(!empty($info['setting'])) {
$modes[] = new Splatoon3GameMode('coop', 'Salmon Run', 0xFFFF5600);
break;
}
}
if(in_array('eggstra', $filters) && array_key_exists('teamContestSchedules', $coop) && !empty($coop['teamContestSchedules']['nodes']))
foreach($coop['teamContestSchedules']['nodes'] as $info) {
if(!empty($info['setting'])) {
$modes[] = new Splatoon3GameMode('eggstra', 'Eggstra Work', 0xFFBE8800);
break;
}
}
if(in_array('bigrun', $filters) && array_key_exists('bigRunSchedules', $coop) && !empty($coop['bigRunSchedules']['nodes']))
foreach($coop['bigRunSchedules']['nodes'] as $info) {
if(!empty($info['setting'])) {
$modes[] = new Splatoon3GameMode('bigrun', 'Big Run', 0xFFB322FF);
break;
}
}
}
return $modes;
}
public function getScheduleFestival(): ?ISplatoonScheduleFestival {
$raw = $this->getRawScheduleData();
if(!array_key_exists('data', $raw) || empty($raw['data']['currentFest']))
return null;
return new Splatoon3ScheduleFestival($this->locale, $raw['data']['currentFest']);
}
public function getSchedules(ISplatoonLocale $locale, array $filters): array {
$raw = $this->getRawScheduleData();
if(!array_key_exists('data', $raw))
return [];
$raw = $raw['data'];
$schedules = [];
if(in_array('regular', $filters) && array_key_exists('regularSchedules', $raw) && !empty($raw['regularSchedules']['nodes']))
foreach($raw['regularSchedules']['nodes'] as $info) {
if(empty($info['regularMatchSetting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryVs($locale, 'regular', '', $info, $info['regularMatchSetting']);
}
$includeRankedSeries = $includeRankedOpen = $includeRanked = in_array('bankara', $filters);
if(!$includeRanked) {
$includeRankedSeries = in_array('series', $filters);
$includeRankedOpen = in_array('open', $filters);
$includeRanked = $includeRankedSeries || $includeRankedOpen;
}
if($includeRanked && array_key_exists('bankaraSchedules', $raw) && !empty($raw['bankaraSchedules']['nodes']))
foreach($raw['bankaraSchedules']['nodes'] as $info) {
if(empty($info['bankaraMatchSettings']))
continue;
if($includeRankedSeries && !empty($info['bankaraMatchSettings'][0]))
$schedules[] = new Splatoon3ScheduleEntryVs($locale, 'bankara', 'CHALLENGE', $info, $info['bankaraMatchSettings'][0]);
if($includeRankedOpen && !empty($info['bankaraMatchSettings'][1]))
$schedules[] = new Splatoon3ScheduleEntryVs($locale, 'bankara', 'OPEN', $info, $info['bankaraMatchSettings'][1]);
}
if(in_array('x', $filters) && array_key_exists('xSchedules', $raw) && !empty($raw['xSchedules']['nodes']))
foreach($raw['xSchedules']['nodes'] as $info) {
if(empty($info['xMatchSetting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryVs($locale, 'x', '', $info, $info['xMatchSetting']);
}
if(in_array('league', $filters) && array_key_exists('eventSchedules', $raw) && !empty($raw['eventSchedules']['nodes']))
foreach($raw['eventSchedules']['nodes'] as $info) {
if(empty($info['leagueMatchSetting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryVsLeague($locale, $info);
}
if(in_array('fest', $filters) && array_key_exists('festSchedules', $raw) && !empty($raw['festSchedules']['nodes'])) {
foreach($raw['festSchedules']['nodes'] as $info) {
if(empty($info['festMatchSettings']))
continue;
$schedules[] = new Splatoon3ScheduleEntryVs($locale, 'fest', '', $info, $info['festMatchSettings']);
}
if(!empty($raw['currentFest']['tricolorStage']))
$schedules[] = new Splatoon3ScheduleEntryVsTricolor($locale, $raw['currentFest']['tricolorStage']);
}
if(array_key_exists('coopGroupingSchedule', $raw)) {
$coop = $raw['coopGroupingSchedule'];
if(in_array('coop', $filters) && array_key_exists('regularSchedules', $coop) && !empty($coop['regularSchedules']['nodes']))
foreach($coop['regularSchedules']['nodes'] as $info) {
if(empty($info['setting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryCoop($locale, 'coop', $info);
}
if(in_array('eggstra', $filters) && array_key_exists('teamContestSchedules', $coop) && !empty($coop['teamContestSchedules']['nodes']))
foreach($coop['teamContestSchedules']['nodes'] as $info) {
if(empty($info['setting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryCoop($locale, 'eggstra', $info);
}
if(in_array('bigrun', $filters) && array_key_exists('bigRunSchedules', $coop) && !empty($coop['bigRunSchedules']['nodes']))
foreach($coop['bigRunSchedules']['nodes'] as $info) {
if(empty($info['setting']))
continue;
$schedules[] = new Splatoon3ScheduleEntryCoop($locale, 'bigrun', $info);
}
}
return $schedules;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonGameMode;
class Splatoon3GameMode implements ISplatoonGameMode {
public function __construct(
private string $name,
private string $title,
private int $colour,
private array $variants = []
) {}
public function getName(): string {
return $this->name;
}
public function getTitle(): string {
return $this->title;
}
public function getColour(): int {
return $this->colour;
}
public function hasVariants(): bool {
return !empty($this->variants);
}
public function getVariants(): array {
return $this->variants;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonGameModeVariant;
class Splatoon3GameModeVariant implements ISplatoonGameModeVariant {
public function __construct(
private string $name,
private string $title,
private int $colour
) {}
public function getName(): string {
return $this->name;
}
public function getTitle(): string {
return $this->title;
}
public function getColour(): int {
return $this->colour;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonLeague;
class Splatoon3League implements ISplatoonLeague {
public function __construct(
private Splatoon3Locale $locale,
private $eventInfo
) {}
public function getId(): string {
return $this->eventInfo['leagueMatchEventId'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('events:%s:name', $this->eventInfo['id']),
$this->eventInfo['name']
);
}
public function getDescription(): string {
return str_replace('<br />', "\n", $this->locale->getString(
sprintf('events:%s:desc', $this->eventInfo['id']),
$this->eventInfo['desc']
));
}
public function getRulesLines(): array {
return explode("\n", str_replace('<br />', "\n", $this->locale->getString(
sprintf('events:%s:regulation', $this->eventInfo['id']),
$this->eventInfo['regulation']
)));
}
public function getRulesUrl(): string {
return $this->eventInfo['regulationUrl'] ?? '';
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonLocale;
class Splatoon3Locale implements ISplatoonLocale {
public function __construct(
private array $strings
) {}
public function getString(string $name, string $fallback): string {
$name = explode(':', $name);
$nameCount = count($name);
if($nameCount > 1 && array_key_exists($name[0], $this->strings) && array_key_exists($name[1], $this->strings[$name[0]])) {
$stringInfo = $this->strings[$name[0]][$name[1]];
if($nameCount > 2) {
if(array_key_exists($name[2], $stringInfo)) {
$stringDeepInfo = $stringInfo[$name[2]];
if(is_string($stringDeepInfo))
return str_replace('<br />', "\n", $stringInfo[$name[2]]);
if(is_array($stringDeepInfo) && $nameCount > 3) {
if(array_key_exists($name[3], $stringDeepInfo)) {
$stringDeepInfo = $stringDeepInfo[$name[3]];
if(is_string($stringDeepInfo))
return $stringDeepInfo;
if(is_array($stringDeepInfo)) {
if($nameCount > 4) {
if(array_key_exists($name[4], $stringDeepInfo)) {
$stringDeepInfo = $stringDeepInfo[$name[4]];
if(is_string($stringDeepInfo))
return $stringDeepInfo;
}
return $fallback;
}
return $stringDeepInfo[array_key_first($stringDeepInfo)];
}
}
}
return $fallback;
}
if(array_key_exists('names', $stringInfo) && array_key_exists($name[2], $stringInfo['names']))
return $stringInfo['names'][$name[2]];
}
if(array_key_exists('name', $stringInfo))
return $stringInfo['name'];
if(!empty($stringInfo))
return $stringInfo[array_key_first($stringInfo)];
}
return $fallback;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Index\XArray;
use Satori\Splatoon\ISplatoonScheduleEntryCoop;
class Splatoon3ScheduleEntryCoop implements ISplatoonScheduleEntryCoop {
public function __construct(
private Splatoon3Locale $locale,
private string $gameMode,
private array $entryInfo
) {}
public function getGameMode(): string {
return $this->gameMode;
}
public function getGameModeVariant(): string {
return '';
}
public function getTimePeriods(): array {
return [
new Splatoon3TimePeriod($this->entryInfo['startTime'], $this->entryInfo['endTime']),
];
}
public function getStages(): array {
return [
new Splatoon3Stage($this->locale, $this->entryInfo['setting']['coopStage']),
];
}
public function getWeapons(): array {
return XArray::select($this->entryInfo['setting']['weapons'], fn($weapon) => new Splatoon3Weapon($this->locale, $weapon));
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Index\XArray;
use Satori\Splatoon\ISplatoonScheduleEntryVs;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon3ScheduleEntryVs implements ISplatoonScheduleEntryVs {
public function __construct(
private Splatoon3Locale $locale,
private string $gameMode,
private string $gameModeVariant,
private array $entryInfo,
private array $matchSetting
) {}
public function getGameMode(): string {
return $this->gameMode;
}
public function getGameModeVariant(): string {
return $this->gameModeVariant;
}
public function getTimePeriods(): array {
return [
new Splatoon3TimePeriod($this->entryInfo['startTime'], $this->entryInfo['endTime']),
];
}
public function getStages(): array {
return XArray::select($this->matchSetting['vsStages'], fn($stage) => new Splatoon3Stage($this->locale, $stage));
}
public function getRuleset(): ISplatoonVsRuleset {
return new Splatoon3VsRuleset($this->locale, $this->matchSetting['vsRule']);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Index\XArray;
use Satori\Splatoon\ISplatoonLeague;
use Satori\Splatoon\ISplatoonScheduleEntryVsLeague;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon3ScheduleEntryVsLeague implements ISplatoonScheduleEntryVsLeague {
public function __construct(
private Splatoon3Locale $locale,
private array $entryInfo
) {}
public function getGameMode(): string {
return 'league';
}
public function getGameModeVariant(): string {
return '';
}
public function getTimePeriods(): array {
return XArray::select($this->entryInfo['timePeriods'], fn($time) => new Splatoon3TimePeriod($time['startTime'], $time['endTime']));
}
public function getStages(): array {
return XArray::select($this->entryInfo['leagueMatchSetting']['vsStages'], fn($stage) => new Splatoon3Stage($this->locale, $stage));
}
public function getRuleset(): ISplatoonVsRuleset {
return new Splatoon3VsRuleset($this->locale, $this->entryInfo['leagueMatchSetting']['vsRule']);
}
public function getLeagueInfo(): ISplatoonLeague {
return new Splatoon3League($this->locale, $this->entryInfo['leagueMatchSetting']['leagueMatchEvent']);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonScheduleEntryVs;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon3ScheduleEntryVsTricolor implements ISplatoonScheduleEntryVs {
public function __construct(
private Splatoon3Locale $locale,
private array $tricolorStage
) {}
public function getGameMode(): string {
return 'tricolor';
}
public function getGameModeVariant(): string {
return '';
}
public function getTimePeriods(): array {
return [
new Splatoon3TimePeriod(strtotime('today'), strtotime('tomorrow')),
];
}
public function getStages(): array {
return [
new Splatoon3Stage($this->locale, $this->tricolorStage),
];
}
public function getRuleset(): ISplatoonVsRuleset {
return new Splatoon3VsRulesetTricolor;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Index\XArray;
use Satori\Splatoon\ISplatoonScheduleFestival;
class Splatoon3ScheduleFestival implements ISplatoonScheduleFestival {
public function __construct(
private Splatoon3Locale $locale,
private array $festInfo
) {}
public function getId(): string {
return $this->festInfo['id'];
}
public function getTitle(): string {
return $this->festInfo['title'];
}
public function getStartTime(): int {
return $this->festInfo['startTime'];
}
public function getMidTermTime(): int {
return $this->festInfo['endTime'];
}
public function getEndTime(): int {
return $this->festInfo['midtermTime'];
}
public function getState(): string {
return $this->festInfo['state'];
}
public function getTeams(): array {
return XArray::select($this->festInfo['teams'], fn($team) => new Splatoon3ScheduleFestivalTeam($team));
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonScheduleFestivalTeam;
class Splatoon3ScheduleFestivalTeam implements ISplatoonScheduleFestivalTeam {
public function __construct(
private array $teamInfo
) {}
public function getId(): string {
return $this->teamInfo['id'];
}
public function getColour(): int {
return (round($this->teamInfo['color']['a'] * 0xFF) << 24)
| (round($this->teamInfo['color']['r'] * 0xFF) << 16)
| (round($this->teamInfo['color']['g'] * 0xFF) << 8)
| round($this->teamInfo['color']['b'] * 0xFF);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonStage;
class Splatoon3Stage implements ISplatoonStage {
public function __construct(
private Splatoon3Locale $locale,
private array $stageInfo
) {}
public function getId(): string {
return $this->stageInfo['vsStageId']
?? $this->stageInfo['id'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('stages:%s:name', $this->stageInfo['id']),
$this->stageInfo['name']
);
}
public function getImage(): string {
return $this->stageInfo['image']['url'];
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonTimePeriod;
class Splatoon3TimePeriod implements ISplatoonTimePeriod {
private int $startTime;
private int $endTime;
public function __construct(string $startTime, string $endTime) {
$this->startTime = strtotime($startTime);
$this->endTime = strtotime($endTime);
}
public function getStartTime(): int {
return $this->startTime;
}
public function getEndTime(): int {
return $this->endTime;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon3VsRuleset implements ISplatoonVsRuleset {
public function __construct(
private Splatoon3Locale $locale,
private array $rulesetInfo
) {}
public function getId(): string {
return $this->rulesetInfo['rule'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('rules:%s:name', $this->rulesetInfo['id']),
$this->rulesetInfo['name']
);
}
public function getShortName(): string {
$id = $this->rulesetInfo['rule'];
if($id === 'LOFT')
return 'TC';
if($id === 'AREA')
return 'SZ';
if($id === 'CLAM')
return 'CB';
if($id === 'GOAL')
return 'RM';
if($id === 'TURF_WAR')
return 'TW';
if($id === 'TEAM_CONTEST')
return 'EW';
return $id;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonVsRuleset;
class Splatoon3VsRulesetTricolor implements ISplatoonVsRuleset {
public function getId(): string {
return 'TRICOLOR_TURF_WAR';
}
public function getName(): string {
return 'Tricolor Turf War';
}
public function getShortName(): string {
return 'TTW';
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Satori\Splatoon\Splatoon3;
use Satori\Splatoon\ISplatoonWeapon;
class Splatoon3Weapon implements ISplatoonWeapon {
public function __construct(
private Splatoon3Locale $locale,
private array $weaponInfo
) {}
public function getId(): string {
return $this->weaponInfo['__splatoon3ink_id'];
}
public function getName(): string {
return $this->locale->getString(
sprintf('weapons:%s:name', $this->weaponInfo['__splatoon3ink_id']),
$this->weaponInfo['name']
);
}
public function getImage(): string {
return $this->weaponInfo['image']['url'];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Satori\Splatoon;
use Syokuhou\IConfig;
class SplatoonContext {
private array $games = [];
public function __construct(private IConfig $config) {
$this->games[] = new Splatoon3\Splatoon3Game;
$this->games[] = new Splatoon2\Splatoon2Game;
}
public function getConfig(): IConfig {
return $this->config;
}
public function getGames(): array {
return $this->games;
}
public function getGameByName(string $name): ?ISplatoonGame {
foreach($this->games as $game)
if($game->getName() === $name)
return $game;
return null;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Satori\Splatoon;
class SplatoonErrorResponse 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,241 @@
<?php
namespace Satori\Splatoon;
use stdClass;
use Index\XArray;
use Index\Routing\Route;
use Index\Routing\RouteHandler;
class SplatoonRoutes extends RouteHandler {
public function __construct(
private SplatoonContext $context
) {}
private static function encodeGameInfo(ISplatoonGame $gameInfo): array {
return [
'id' => $gameInfo->getName(),
'name' => $gameInfo->getTitle(),
'colour' => $gameInfo->getColour(),
'variant' => $gameInfo->getName(),
];
}
private static function encodeStageInfo(ISplatoonStage $stageInfo): array {
return [
'id' => $stageInfo->getId(),
'name' => $stageInfo->getName(),
'image' => $stageInfo->getImage(),
];
}
#[Route('GET', '/splatoon.php')]
public function getPHP($response, $request) {
$gameInfo = $this->context->getGameByName((string)$request->getParam('g'));
if($gameInfo === null)
return new SplatoonErrorResponse('game:id');
$localeId = $gameInfo->getLocaleAlias((string)$request->getParam('l'));
if(!$gameInfo->isSupportedLocale($localeId))
return new SplatoonErrorResponse('locale:id');
$localeInfo = $gameInfo->createLocale($localeId);
$typeId = (string)$request->getParam('t');
if($typeId === 'schedules' && $gameInfo instanceof ISplatoonHasSchedules) {
$filters = array_filter(explode(',', (string)$request->getParam('sf')));
$filterAll = empty($filters);
if($filterAll)
$filters = $gameInfo->getScheduleFilters();
else
foreach($filters as &$filter) {
$filter = $gameInfo->getScheduleFilterAlias($filter);
if(!$gameInfo->isValidScheduleFilter($filter))
return new SplatoonErrorResponse('schedules:filter');
}
// can't really test this while there's no splatfest going on, test it later
$fest = null;
$festInfo = $gameInfo->getScheduleFestival();
if($festInfo !== null) {
$fest = new stdClass;
$fest->id = $festInfo->getId();
$fest->title = $festInfo->getTitle();
$fest->start = $festInfo->getStartTime();
$fest->midterm = $festInfo->getMidTermTime();
$fest->end = $festInfo->getEndTime();
$fest->state = $festInfo->getState();
$fest->teams = XArray::select($festInfo->getTeams(), function($teamInfo) {
$team = new stdClass;
$team->id = $teamInfo->getId();
$team->colour = $teamInfo->getColour();
return $team;
});
}
$modes = XArray::select($gameInfo->getScheduleModes($localeInfo, $filters), function($modeInfo) {
$mode = new stdClass;
$mode->id = $modeInfo->getName();
$mode->name = $modeInfo->getTitle();
$mode->colour = $modeInfo->getColour();
if($modeInfo->hasVariants())
$mode->variants = XArray::select($modeInfo->getVariants(), function($variantInfo) {
$variant = new stdClass;
$variant->id = $variantInfo->getName();
$variant->name = $variantInfo->getTitle();
$variant->colour = $variantInfo->getColour();
return $variant;
});
return $mode;
});
$schedules = [];
$scheduleInfos = $gameInfo->getSchedules($localeInfo, $filters);
foreach($scheduleInfos as $scheduleInfo) {
if($scheduleInfo instanceof ISplatoonScheduleEntry) {
$gameMode = $scheduleInfo->getGameMode() . ':' . $scheduleInfo->getGameModeVariant();
if(!array_key_exists($gameMode, $schedules)) {
$schedules[$gameMode] = $scheduleMode = new stdClass;
$scheduleMode->mode = $scheduleInfo->getGameMode();
$variant = $scheduleInfo->getGameModeVariant();
if($variant !== '')
$scheduleMode->variant = $variant;
$scheduleMode->schedule = [];
}
$schedules[$gameMode]->schedule[] = $schedule = new stdClass;
$schedule->periods = XArray::select($scheduleInfo->getTimePeriods(), function($periodInfo) {
$period = new stdClass;
$period->start = str_replace('+00:00', 'Z', date(\DateTime::ATOM, $periodInfo->getStartTime()));
$period->end = str_replace('+00:00', 'Z', date(\DateTime::ATOM, $periodInfo->getEndTime()));
return $period;
});
$stagesInfo = $scheduleInfo->getStages();
if(!empty($stagesInfo))
$schedule->stages = XArray::select($stagesInfo, fn($item) => self::encodeStageInfo($item));
$schedule->flags = [];
if($scheduleInfo instanceof ISplatoonScheduleEntryVs) {
$schedule->variant = 'vs';
$rulesetInfo = $scheduleInfo->getRuleset();
$schedule->ruleset = new stdClass;
$schedule->ruleset->id = $rulesetInfo->getId();
$schedule->ruleset->name = $rulesetInfo->getName();
$schedule->ruleset->short = $rulesetInfo->getShortName();
if($scheduleInfo instanceof ISplatoonScheduleEntryVsLeague) {
$schedule->flags[] = 'league';
$leagueInfo = $scheduleInfo->getLeagueInfo();
$schedule->league = new stdClass;
$schedule->league->id = $leagueInfo->getId();
$schedule->league->name = $leagueInfo->getName();
$schedule->league->desc = $leagueInfo->getDescription();
$schedule->league->rules = new stdClass;
$schedule->league->rules->lines = $leagueInfo->getRulesLines();
$schedule->league->rules->url = $leagueInfo->getRulesUrl();
}
} elseif($scheduleInfo instanceof ISplatoonScheduleEntryCoop) {
$schedule->variant = 'coop';
$weaponsInfo = $scheduleInfo->getWeapons();
if(!empty($weaponsInfo))
$schedule->weapons = XArray::select($weaponsInfo, function($weaponInfo) {
$weapon = new stdClass;
$weapon->id = $weaponInfo->getId();
$weapon->name = $weaponInfo->getName();
$weapon->image = $weaponInfo->getImage();
return $weapon;
});
}
}
}
return [
'game' => self::encodeGameInfo($gameInfo),
'fest' => $fest,
'modes' => $modes,
'schedules' => array_values($schedules),
];
}
if($typeId === 'fests' && $gameInfo instanceof ISplatoonHasFestivals) {
$regions = array_filter(explode(',', (string)$request->getParam('fr')));
$regionsAll = empty($regions);
if($regionsAll)
$regions = $gameInfo->getFestivalRegions();
else
foreach($regions as &$region) {
$region = $gameInfo->getFestivalRegionAlias($region);
if(!$gameInfo->isSupportedFestivalRegion($region))
return new SplatoonErrorResponse('fests:region');
}
$fests = [];
$festInfos = $gameInfo->getFestivals($localeInfo, $regions);
foreach($festInfos as $festInfo) {
if(array_key_exists($festInfo->getId(), $fests)) {
$group = $fests[$festInfo->getId()];
} else {
$fests[$festInfo->getId()] = $group = new stdClass;
$group->regions = [];
}
$group->regions[] = $fest = new stdClass;
$fest->id = $festInfo->getLocalId();
$fest->region = $festInfo->getRegion();
$fest->state = $festInfo->getState();
$fest->start = str_replace('+00:00', 'Z', date(\DateTime::ATOM, $festInfo->getStartTime()));
$fest->end = str_replace('+00:00', 'Z', date(\DateTime::ATOM, $festInfo->getEndTime()));
$fest->title = $festInfo->getTitle();
$fest->image = $festInfo->getImage();
$fest->can_vote = $festInfo->canVote();
$fest->teams = XArray::select($festInfo->getTeams(), function($teamInfo) {
$team = new stdClass;
$team->id = $teamInfo->getLocalId();
$team->name = $teamInfo->getName();
$team->image = $teamInfo->getImage();
$team->colour = $teamInfo->getColour();
$team->has_results = $teamInfo->hasResults();
if($team->has_results) {
$team->is_winner = $teamInfo->isWinner();
$team->results = XArray::select($teamInfo->getResults(), function($resultInfo) {
$result = new stdClass;
$result->category = $resultInfo->getCategory();
$result->ratio = $resultInfo->getRatio();
$result->is_top = $resultInfo->isTop();
return $result;
});
}
return $team;
});
if($festInfo->hasSpecialStages())
$fest->special_stages = XArray::select($festInfo->getSpecialStages(), fn($item) => self::encodeStageInfo($item));
}
return [
'game' => self::encodeGameInfo($gameInfo),
'fests' => array_values($fests),
];
}
return new SplatoonErrorResponse('type:id');
}
}

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,99 @@
<?php
namespace Satori\Translation;
use DOMDocument;
use Index\Routing\Route;
use Index\Routing\RouteHandler;
class TranslateRoutes extends RouteHandler {
public function __construct(
private TranslateContext $context
) {}
#[Route('GET', '/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 '';
}
}

34
tools/migrate Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../satori.php';
try {
touch(SAT_ROOT . '/.migrating');
chmod(SAT_ROOT . '/.migrating', 0777);
$db = $sat->getDatabase();
echo 'Creating migration manager...' . PHP_EOL;
$manager = $db->createMigrationManager();
echo 'Preparing to run migrations...' . PHP_EOL;
$manager->init();
echo 'Creating migration repository...' . PHP_EOL;
$repo = $db->createMigrationRepo();
echo 'Running migrations...' . PHP_EOL;
$completed = $manager->processMigrations($repo);
if(empty($completed)) {
echo 'There were no migrations to run!' . PHP_EOL;
} else {
echo 'The following migrations have been completed:' . PHP_EOL;
foreach($completed as $migration)
echo ' - ' . $migration . PHP_EOL;
}
echo PHP_EOL;
} finally {
unlink(SAT_ROOT . '/.migrating');
}

26
tools/new-migration Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env php
<?php
use Index\Data\Migration\FsDbMigrationRepo;
require_once __DIR__ . '/../satori.php';
$db = $sat->getDatabase();
$repo = $db->createMigrationRepo();
if(!($repo instanceof FsDbMigrationRepo)) {
echo 'Migration repository type does not support creation of templates.' . PHP_EOL;
return;
}
$baseName = implode(' ', array_slice($argv, 1));
$manager = $db->createMigrationManager();
try {
$names = $manager->createNames($baseName);
} catch(InvalidArgumentException $ex) {
echo $ex->getMessage() . PHP_EOL;
return;
}
$repo->saveMigrationTemplate($names->name, $manager->template($names->className));
echo "Template for '{$names->className}' has been saved to {$names->name}.php." . PHP_EOL;