From f491be9eba815568b7d9529986d9d1cf6ced6d26 Mon Sep 17 00:00:00 2001 From: flashwave Date: Mon, 6 Nov 2023 00:45:15 +0000 Subject: [PATCH] Satori API scripts overhaul! --- .gitignore | 10 +- composer.json | 13 + composer.lock | 107 ++ .../2023_11_06_004121_initial_structure.php | 24 + public/404.html | 47 + public/booru.php | 186 --- public/exrate.php | 93 -- public/git-broadcast.php | 167 --- public/index.php | 69 +- public/splatoon.php | 1018 ----------------- public/translate.php | 150 --- public/word-define.php | 39 - satori.php | 25 + src/Booru/BooruContext.php | 30 + src/Booru/BooruErrorResponse.php | 16 + src/Booru/BooruQueryResponse.php | 24 + src/Booru/BooruRoutes.php | 60 + src/Booru/BooruSourceInfo.php | 25 + src/Booru/Danbooru/DanbooruPost.php | 52 + src/Booru/Danbooru/DanbooruSource.php | 62 + src/Booru/Gelbooru/GelbooruPost.php | 32 + src/Booru/Gelbooru/GelbooruSource.php | 59 + src/Booru/IBooruPost.php | 12 + src/Booru/IBooruPostWithSpecificTags.php | 10 + src/Booru/IBooruSource.php | 7 + src/Booru/Konachan/KonachanPost.php | 37 + src/Booru/Konachan/KonachanSource.php | 59 + src/Booru/Yandere/YanderePost.php | 32 + src/Booru/Yandere/YandereSource.php | 78 ++ src/DatabaseContext.php | 30 + src/Dictionary/DictionaryContext.php | 34 + src/Dictionary/DictionaryResult.php | 42 + src/Dictionary/DictionaryRoutes.php | 65 ++ src/ExRate/ExRateContext.php | 68 ++ src/ExRate/ExRateDatabase.php | 49 + src/ExRate/ExRateRoutes.php | 93 ++ src/RoutingContext.php | 36 + src/SFileCache.php | 23 + src/SHttp.php | 32 + src/SatoriContext.php | 67 ++ src/Splatoon/ISplatoonFestival.php | 20 + src/Splatoon/ISplatoonFestivalTeam.php | 14 + src/Splatoon/ISplatoonFestivalTeamResult.php | 8 + src/Splatoon/ISplatoonGame.php | 14 + src/Splatoon/ISplatoonGameMode.php | 10 + src/Splatoon/ISplatoonGameModeVariant.php | 8 + src/Splatoon/ISplatoonHasFestivals.php | 9 + src/Splatoon/ISplatoonHasSchedules.php | 11 + src/Splatoon/ISplatoonLeague.php | 10 + src/Splatoon/ISplatoonLocale.php | 6 + src/Splatoon/ISplatoonScheduleEntry.php | 9 + src/Splatoon/ISplatoonScheduleEntryCoop.php | 6 + src/Splatoon/ISplatoonScheduleEntryVs.php | 6 + .../ISplatoonScheduleEntryVsLeague.php | 6 + src/Splatoon/ISplatoonScheduleFestival.php | 14 + .../ISplatoonScheduleFestivalTeam.php | 7 + src/Splatoon/ISplatoonStage.php | 8 + src/Splatoon/ISplatoonTimePeriod.php | 7 + src/Splatoon/ISplatoonVsRuleset.php | 8 + src/Splatoon/ISplatoonWeapon.php | 8 + src/Splatoon/Splatoon2/Splatoon2CoopStage.php | 26 + src/Splatoon/Splatoon2/Splatoon2Festival.php | 122 ++ .../Splatoon2/Splatoon2FestivalTeam.php | 58 + .../Splatoon2/Splatoon2FestivalTeamResult.php | 24 + src/Splatoon/Splatoon2/Splatoon2Game.php | 243 ++++ src/Splatoon/Splatoon2/Splatoon2GameMode.php | 36 + src/Splatoon/Splatoon2/Splatoon2Locale.php | 42 + .../Splatoon2/Splatoon2ScheduleEntryCoop.php | 42 + .../Splatoon2/Splatoon2ScheduleEntryVs.php | 43 + src/Splatoon/Splatoon2/Splatoon2Stage.php | 26 + .../Splatoon2/Splatoon2TimePeriod.php | 19 + src/Splatoon/Splatoon2/Splatoon2VsRuleset.php | 48 + src/Splatoon/Splatoon2/Splatoon2Weapon.php | 26 + src/Splatoon/Splatoon3/Splatoon3Festival.php | 76 ++ .../Splatoon3/Splatoon3FestivalTeam.php | 71 ++ .../Splatoon3/Splatoon3FestivalTeamResult.php | 24 + src/Splatoon/Splatoon3/Splatoon3Game.php | 342 ++++++ src/Splatoon/Splatoon3/Splatoon3GameMode.php | 33 + .../Splatoon3/Splatoon3GameModeVariant.php | 24 + src/Splatoon/Splatoon3/Splatoon3League.php | 40 + src/Splatoon/Splatoon3/Splatoon3Locale.php | 64 ++ .../Splatoon3/Splatoon3ScheduleEntryCoop.php | 37 + .../Splatoon3/Splatoon3ScheduleEntryVs.php | 38 + .../Splatoon3ScheduleEntryVsLeague.php | 38 + .../Splatoon3ScheduleEntryVsTricolor.php | 36 + .../Splatoon3/Splatoon3ScheduleFestival.php | 40 + .../Splatoon3ScheduleFestivalTeam.php | 21 + src/Splatoon/Splatoon3/Splatoon3Stage.php | 27 + .../Splatoon3/Splatoon3TimePeriod.php | 22 + src/Splatoon/Splatoon3/Splatoon3VsRuleset.php | 39 + .../Splatoon3/Splatoon3VsRulesetTricolor.php | 18 + src/Splatoon/Splatoon3/Splatoon3Weapon.php | 26 + src/Splatoon/SplatoonContext.php | 28 + src/Splatoon/SplatoonErrorResponse.php | 16 + src/Splatoon/SplatoonRoutes.php | 241 ++++ src/Translation/TranslateContext.php | 85 ++ src/Translation/TranslateRoutes.php | 99 ++ tools/migrate | 34 + tools/new-migration | 26 + 99 files changed, 3867 insertions(+), 1704 deletions(-) create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 database/2023_11_06_004121_initial_structure.php create mode 100644 public/404.html delete mode 100644 public/booru.php delete mode 100644 public/exrate.php delete mode 100644 public/git-broadcast.php delete mode 100644 public/splatoon.php delete mode 100644 public/translate.php delete mode 100644 public/word-define.php create mode 100644 satori.php create mode 100644 src/Booru/BooruContext.php create mode 100644 src/Booru/BooruErrorResponse.php create mode 100644 src/Booru/BooruQueryResponse.php create mode 100644 src/Booru/BooruRoutes.php create mode 100644 src/Booru/BooruSourceInfo.php create mode 100644 src/Booru/Danbooru/DanbooruPost.php create mode 100644 src/Booru/Danbooru/DanbooruSource.php create mode 100644 src/Booru/Gelbooru/GelbooruPost.php create mode 100644 src/Booru/Gelbooru/GelbooruSource.php create mode 100644 src/Booru/IBooruPost.php create mode 100644 src/Booru/IBooruPostWithSpecificTags.php create mode 100644 src/Booru/IBooruSource.php create mode 100644 src/Booru/Konachan/KonachanPost.php create mode 100644 src/Booru/Konachan/KonachanSource.php create mode 100644 src/Booru/Yandere/YanderePost.php create mode 100644 src/Booru/Yandere/YandereSource.php create mode 100644 src/DatabaseContext.php create mode 100644 src/Dictionary/DictionaryContext.php create mode 100644 src/Dictionary/DictionaryResult.php create mode 100644 src/Dictionary/DictionaryRoutes.php create mode 100644 src/ExRate/ExRateContext.php create mode 100644 src/ExRate/ExRateDatabase.php create mode 100644 src/ExRate/ExRateRoutes.php create mode 100644 src/RoutingContext.php create mode 100644 src/SFileCache.php create mode 100644 src/SHttp.php create mode 100644 src/SatoriContext.php create mode 100644 src/Splatoon/ISplatoonFestival.php create mode 100644 src/Splatoon/ISplatoonFestivalTeam.php create mode 100644 src/Splatoon/ISplatoonFestivalTeamResult.php create mode 100644 src/Splatoon/ISplatoonGame.php create mode 100644 src/Splatoon/ISplatoonGameMode.php create mode 100644 src/Splatoon/ISplatoonGameModeVariant.php create mode 100644 src/Splatoon/ISplatoonHasFestivals.php create mode 100644 src/Splatoon/ISplatoonHasSchedules.php create mode 100644 src/Splatoon/ISplatoonLeague.php create mode 100644 src/Splatoon/ISplatoonLocale.php create mode 100644 src/Splatoon/ISplatoonScheduleEntry.php create mode 100644 src/Splatoon/ISplatoonScheduleEntryCoop.php create mode 100644 src/Splatoon/ISplatoonScheduleEntryVs.php create mode 100644 src/Splatoon/ISplatoonScheduleEntryVsLeague.php create mode 100644 src/Splatoon/ISplatoonScheduleFestival.php create mode 100644 src/Splatoon/ISplatoonScheduleFestivalTeam.php create mode 100644 src/Splatoon/ISplatoonStage.php create mode 100644 src/Splatoon/ISplatoonTimePeriod.php create mode 100644 src/Splatoon/ISplatoonVsRuleset.php create mode 100644 src/Splatoon/ISplatoonWeapon.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2CoopStage.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2Festival.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2FestivalTeam.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2FestivalTeamResult.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2Game.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2GameMode.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2Locale.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2ScheduleEntryCoop.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2ScheduleEntryVs.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2Stage.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2TimePeriod.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2VsRuleset.php create mode 100644 src/Splatoon/Splatoon2/Splatoon2Weapon.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3Festival.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3FestivalTeam.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3FestivalTeamResult.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3Game.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3GameMode.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3GameModeVariant.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3League.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3Locale.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleEntryCoop.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVs.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsLeague.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsTricolor.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleFestival.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3ScheduleFestivalTeam.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3Stage.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3TimePeriod.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3VsRuleset.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3VsRulesetTricolor.php create mode 100644 src/Splatoon/Splatoon3/Splatoon3Weapon.php create mode 100644 src/Splatoon/SplatoonContext.php create mode 100644 src/Splatoon/SplatoonErrorResponse.php create mode 100644 src/Splatoon/SplatoonRoutes.php create mode 100644 src/Translation/TranslateContext.php create mode 100644 src/Translation/TranslateRoutes.php create mode 100755 tools/migrate create mode 100755 tools/new-migration diff --git a/.gitignore b/.gitignore index 293c98f..ad1ae10 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fde3add --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Satori\\": "src" + } + }, + "require": { + "flashwave/index": "dev-master", + "flashwave/syokuhou": "dev-master" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7d04793 --- /dev/null +++ b/composer.lock @@ -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" +} diff --git a/database/2023_11_06_004121_initial_structure.php b/database/2023_11_06_004121_initial_structure.php new file mode 100644 index 0000000..e1b2848 --- /dev/null +++ b/database/2023_11_06_004121_initial_structure.php @@ -0,0 +1,24 @@ +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" + '); + } +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..c044636 --- /dev/null +++ b/public/404.html @@ -0,0 +1,47 @@ + + + + + Cannot find page + + + +
+

+ + The page cannot be found +

+

+ The page you are looking for might have been removed, had its + name changed, or is temporarily unavailable. +

+
+

+ Please try the following: +

+ +

+ HTTP 404 - File not found +
+ Internet Explorer +

+
+ + diff --git a/public/booru.php b/public/booru.php deleted file mode 100644 index 5268bb7..0000000 --- a/public/booru.php +++ /dev/null @@ -1,186 +0,0 @@ - [ - '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, -]); diff --git a/public/exrate.php b/public/exrate.php deleted file mode 100644 index 36a9d02..0000000 --- a/public/exrate.php +++ /dev/null @@ -1,93 +0,0 @@ -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); diff --git a/public/git-broadcast.php b/public/git-broadcast.php deleted file mode 100644 index 11319ff..0000000 --- a/public/git-broadcast.php +++ /dev/null @@ -1,167 +0,0 @@ - $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); -} diff --git a/public/index.php b/public/index.php index 5678434..60af270 100644 --- a/public/index.php +++ b/public/index.php @@ -1,50 +1,21 @@ - - - - - Cannot find page - - - -
-

- - The page cannot be found -

-

- The page you are looking for might have been removed, had its - name changed, or is temporarily unavailable. -

-
-

- Please try the following: -

- -

- HTTP 404 - File not found -
- Internet Explorer -

-
- - \ No newline at end of file +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(); diff --git a/public/splatoon.php b/public/splatoon.php deleted file mode 100644 index 53a2f3a..0000000 --- a/public/splatoon.php +++ /dev/null @@ -1,1018 +0,0 @@ - 'eu', 'JP' => 'jp', 'US' => 'na', -]); -define('SP2_FEST_REGIONS_ALIAS_REVERSE', [ - 'eu' => 'EU', 'jp' => 'JP', 'na' => 'US', -]); -define('SP2_LOCALES_ALIAS', [ - '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', -]); - -define('SP_GAME_SP3', 's3'); -define('SP_GAME_SP2', 's2'); -define('SP_GAMES', [SP_GAME_SP3, SP_GAME_SP2]); - -define('SP_GAME_INFOS', [ - SP_GAME_SP3 => [ - 'name' => 'Splatoon 3', - 'colour' => 0xFFEFFE65, - 'variant' => SP_GAME_SP3, - 'types' => SP3_TYPES, - 'locales' => SP3_LOCALES, - 'locale' => SP3_DEFAULT_LOCALE, - 'localeUrlFormat' => SP3_LOCALE, - 'schedUrls' => ['all' => SP3_SCHED], - 'schedFilters' => SP3_SCHED_FILTER, - 'festUrl' => SP3_FESTS, - 'festRegions' => SP3_FEST_REGIONS, - ], - SP_GAME_SP2 => [ - 'name' => 'Splatoon 2', - 'colour' => 0xFFFF4506, - 'variant' => SP_GAME_SP2, - 'types' => SP2_TYPES, - 'locales' => SP2_LOCALES, - 'localeAliases' => SP2_LOCALES_ALIAS, - 'locale' => SP2_DEFAULT_LOCALE, - 'localeUrlFormat' => SP2_LOCALE, - 'schedUrls' => ['vs' => SP2_SCHED, 'co' => SP2_COOP_SCHED], - 'schedFilters' => SP2_SCHED_FILTER, - 'festUrl' => SP2_FESTS, - 'festRegions' => SP2_FEST_REGIONS, - 'festRegionAliases' => SP2_FEST_REGIONS_ALIAS, - 'festRegionAliasesReverse' => SP2_FEST_REGIONS_ALIAS_REVERSE, - ], -]); - -function httpRequest(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 => 'Satori/20230203 (+https://fii.moe/satori)', - CURLOPT_HTTPHEADER => ['Accept: application/json'], - ]); - $response = json_decode(curl_exec($curl)); - curl_close($curl); - - return $response; -} - -function cached(string $name, callable $match, callable $create): mixed { - $fileName = sys_get_temp_dir() . '/satori-splatoon-' . $name . '.tmp'; - - if(is_file($fileName) && $match(filemtime($fileName), $fileName)) - return unserialize(file_get_contents($fileName)); - - $output = $create(); - file_put_contents($fileName, serialize($output)); - return $output; -} - -function cache_lifetime_15min($mtime): bool { - return is_int($mtime) - && floor($mtime / 900) >= floor(time() / 900); -} - -function splatoon2_format_date(int $time): string { - return str_replace('+00:00', 'Z', date(DATE_ISO8601_EXPANDED, $time)); -} - -function splatoon2_translate_ruleset(string $id): string { - 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; -} - -function splatoon2_get_mode_colour(string $id): int { - if($id === 'regular') - return 0xFF19D719; - if($id === 'gachi') - return 0xFFF54910; - if($id === 'league') - return 0xFFF02D7D; - if($id === 'coop') - return 0xFFFF5600; - return 0xFFFFFFFF; -} - -function splatoon2_ruleset_short(string $id): string { - 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'; - if($id === 'coop') - return 'SR'; - return $id; -} - -function splatoon3_ruleset_short(string $id): string { - 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'; - if($id === 'coop') - return 'SR'; - if($id === 'bigrun') - return 'BR'; - return $id; -} -date_default_timezone_set('utc'); -header('Content-Type: application/json; charset=utf-8'); - -$gameId = (string)filter_input(INPUT_GET, 'g'); -if(!in_array($gameId, SP_GAMES)) - die('{"error":"game:id"}'); - -$gameInfo = SP_GAME_INFOS[$gameId] ?? null; -if(!isset($gameInfo)) - die('{"error":"game:info"}'); - -$typeId = (string)filter_input(INPUT_GET, 't'); -if(empty($typeId) || !in_array($typeId, $gameInfo['types'])) - die('{"error":"type:id"}'); - -$localeId = (string)filter_input(INPUT_GET, 'l'); -if(empty($localeId)) - $localeId = $gameInfo['locale']; -elseif(!in_array($localeId, $gameInfo['locales'])) { - if(isset($gameInfo['localeAliases']) && array_key_exists($localeId, $gameInfo['localeAliases'])) - $localeId = $gameInfo['localeAliases'][$localeId]; - else die('{"error":"locale:id"}'); -} - -$localeInfo = cached(sprintf('%s-locale-%s', $gameId, $localeId), 'cache_lifetime_15min', function() use($gameInfo, $localeId) { - if(empty($gameInfo['localeUrlFormat'])) - die('{"error":"locale:url"}'); - - $localeInfo = httpRequest(sprintf($gameInfo['localeUrlFormat'], $localeId)); - if(!is_object($localeInfo)) - die('{"error":"locale:info"}'); - - return $localeInfo; -}); - -if($typeId === SP_TYPE_SCHED) { // SCHEDULE SHIT - $filters = array_filter(explode(',', (string)filter_input(INPUT_GET, 'sf'))); - $filterAll = empty($filters); - foreach($filters as $filter) - if(!in_array($filter, $gameInfo['schedFilters'])) - die('{"error":"schedules:filter"}'); - - $schedRaw = (object)cached(sprintf('%s-sched', $gameId), 'cache_lifetime_15min', function() use($gameInfo) { - if(empty($gameInfo['schedUrls'])) - die('{"error":"schedules:url"}'); - - $output = []; - foreach($gameInfo['schedUrls'] as $name => $url) - $output[$name] = httpRequest($url); - - return $output; - }); - - $gameModes = []; - $schedules = []; - $currentFest = null; - - if($gameInfo['variant'] === SP_GAME_SP3) { - $data = $schedRaw->all->data; - - $includeRegular = $filterAll || in_array('regular', $filters); - $includeSeries = $filterAll || in_array('series', $filters); - $includeOpen = $filterAll || in_array('open', $filters); - $includeRanked = $filterAll || in_array('bankara', $filters); - $includeX = $filterAll || in_array('x', $filters); - $includeLeague = $filterAll || in_array('league', $filters); - $includeSalmon = $filterAll || in_array('coop', $filters); - $includeEggstra = $filterAll || in_array('eggstra', $filters); - $includeBigRun = $filterAll || in_array('bigrun', $filters); - $includeFest = $filterAll || in_array('fest', $filters); - $includeSeries = $includeSeries || $includeRanked; - $includeOpen = $includeOpen || $includeRanked; - $includeRanked = $includeSeries || $includeOpen; - - function splatoon3_schedule_gen($target, $node): void { - $target->periods = [[ - 'start' => $node->startTime, - 'end' => $node->endTime, - ]]; - } - - function splatoon3_schedule_vs($target, $settings): void { - global $localeInfo; - - $target->variant = 'vs'; - $target->flags = []; - - $target->ruleset = [ - 'id' => $settings->vsRule->rule, - 'name' => $localeInfo->rules->{$settings->vsRule->id}?->name ?? $settings->vsRule->name, - 'short' => splatoon3_ruleset_short($settings->vsRule->rule), - ]; - - $target->stages = []; - foreach($settings->vsStages as $stage) - $target->stages[] = [ - 'id' => (string)$stage->vsStageId, - 'name' => $localeInfo->stages->{$stage->id}?->name ?? $stage->name, - 'image' => $stage->image->url, - ]; - } - - function splatoon3_schedule_periods($target, $periods): void { - $target->periods = []; - foreach($periods as $period) - $target->periods[] = [ - 'start' => $period->startTime, - 'end' => $period->endTime, - ]; - } - - if($includeRegular && !empty($data->regularSchedules->nodes)) { - $schedule = []; - - foreach($data->regularSchedules->nodes as $node) { - if(empty($node->regularMatchSetting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - splatoon3_schedule_vs($item, $node->regularMatchSetting); - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'regular', - 'name' => 'Regular Battle', - 'colour' => 0xFF19D719, - ]; - - $schedules[] = [ - 'mode' => 'regular', - 'schedule' => $schedule, - ]; - } - } - - if($includeRanked && !empty($data->bankaraSchedules->nodes)) { - $series = []; - $open = []; - - foreach($data->bankaraSchedules->nodes as $node) { - if(empty($node->bankaraMatchSettings)) - continue; - - foreach($node->bankaraMatchSettings as $settings) { - if($settings->mode === 'CHALLENGE') { - $series[] = $item = new stdClass; - } elseif($settings->mode === 'OPEN') { - $open[] = $item = new stdClass; - } else continue; - - splatoon3_schedule_gen($item, $node); - splatoon3_schedule_vs($item, $settings); - } - } - - if($includeSeries && !empty($series)) { - if(empty($gameModes['bankara'])) - $gameModes['bankara'] = [ - 'id' => 'bankara', - 'name' => 'Anarchy Battle', - 'colour' => 0xFFF54910, - 'variants' => [], - ]; - $gameModes['bankara']['variants'][] = [ - 'id' => 'CHALLENGE', - 'name' => 'Series', - 'colour' => 0xFF603BFF, - ]; - - $schedules[] = [ - 'mode' => 'bankara', - 'variant' => 'CHALLENGE', - 'schedule' => $series, - ]; - } - - if($includeOpen && !empty($open)) { - if(empty($gameModes['bankara'])) - $gameModes['bankara'] = [ - 'id' => 'bankara', - 'name' => 'Anarchy Battle', - 'colour' => 0xFFF54910, - 'variants' => [], - ]; - $gameModes['bankara']['variants'][] = [ - 'id' => 'OPEN', - 'name' => 'Open', - 'colour' => 0xFF603BFF, - ]; - - $schedules[] = [ - 'mode' => 'bankara', - 'variant' => 'OPEN', - 'schedule' => $open, - ]; - } - } - - if($includeX && !empty($data->xSchedules->nodes)) { - $schedule = []; - - foreach($data->xSchedules->nodes as $node) { - if(empty($node->xMatchSetting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - splatoon3_schedule_vs($item, $node->xMatchSetting); - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'x', - 'name' => 'X Battle', - 'colour' => 0xFF0FDB9B, - ]; - - $schedules[] = [ - 'mode' => 'x', - 'schedule' => $schedule, - ]; - } - } - - if($includeLeague && !empty($data->eventSchedules->nodes)) { - $schedule = []; - - foreach($data->eventSchedules->nodes as $node) { - if(empty($node->leagueMatchSetting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_periods($item, $node->timePeriods); - splatoon3_schedule_vs($item, $node->leagueMatchSetting); - $item->flags[] = 'league'; - - if(!empty($node->leagueMatchSetting->leagueMatchEvent)) { - $event = $node->leagueMatchSetting->leagueMatchEvent; - $eventLocale = $localeInfo->events->{$event->id}; - - $item->league = [ - 'id' => $event->leagueMatchEventId, - 'name' => $eventLocale->name ?? $event->name, - 'desc' => $eventLocale->desc ?? $event->desc, - 'rules' => [ - 'lines' => explode('
', $eventLocale->regulation ?? $event->regulation), - 'url' => $event->regulationUrl, - ], - ]; - } - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'league', - 'name' => 'Challenge Battle', - 'colour' => 0xFFF02D7D, - ]; - - $schedules[] = [ - 'mode' => 'league', - 'schedule' => $schedule, - ]; - } - } - - if($includeFest && !empty($data->festSchedules->nodes)) { - $schedule = []; - - foreach($data->festSchedules->nodes as $node) { - if(empty($node->festMatchSetting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - splatoon3_schedule_vs($item, $node->festMatchSetting); - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'fest', - 'name' => 'Splatfest Battle', - 'colour' => 0xFF71717A, - ]; - - $schedules[] = [ - 'mode' => 'fest', - 'schedule' => $schedule, - ]; - } - } - - if($includeSalmon && !empty($data->coopGroupingSchedule->regularSchedules->nodes)) { - $schedule = []; - - $nodes = $data->coopGroupingSchedule->regularSchedules->nodes; - foreach($nodes as $node) { - if(empty($node->setting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - $item->variant = 'coop'; - - $item->stages = [ - [ - 'name' => $node->setting->coopStage->name, - 'image' => $node->setting->coopStage->image->url, - ], - ]; - - $item->weapons = []; - foreach($node->setting->weapons as $weapon) - $item->weapons[] = [ - 'name' => $localeInfo->weapons->{$weapon->__splatoon3ink_id}?->name ?? $weapon->name, - 'image' => $weapon->image->url, - ]; - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'coop', - 'name' => 'Salmon Run', - 'colour' => 0xFFFF5600, - ]; - - $schedules[] = [ - 'mode' => 'coop', - 'schedule' => $schedule, - ]; - } - } - - if($includeEggstra && !empty($data->coopGroupingSchedule->teamContestSchedules->nodes)) { - $schedule = []; - - $nodes = $data->coopGroupingSchedule->teamContestSchedules->nodes; - foreach($nodes as $node) { - if(empty($node->setting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - $item->variant = 'coop'; - - $item->stages = [ - [ - 'name' => $node->setting->coopStage->name, - 'image' => $node->setting->coopStage->image->url, - ], - ]; - - $item->weapons = []; - foreach($node->setting->weapons as $weapon) - $item->weapons[] = [ - 'name' => $localeInfo->weapons->{$weapon->__splatoon3ink_id}?->name ?? $weapon->name, - 'image' => $weapon->image->url, - ]; - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'eggstra', - 'name' => 'Eggstra Work', - 'colour' => 0xFFBE8800, - ]; - - $schedules[] = [ - 'mode' => 'eggstra', - 'schedule' => $schedule, - ]; - } - } - - if($includeBigRun && !empty($data->coopGroupingSchedule->bigRunSchedules->nodes)) { - $schedule = []; - - $nodes = $data->coopGroupingSchedule->bigRunSchedules->nodes; - foreach($nodes as $node) { - if(empty($node->setting)) - continue; - - $schedule[] = $item = new stdClass; - splatoon3_schedule_gen($item, $node); - $item->variant = 'coop'; - - $item->stages = [ - [ - 'name' => $node->setting->coopStage->name, - 'image' => $node->setting->coopStage->image->url, - ], - ]; - - $item->weapons = []; - foreach($node->setting->weapons as $weapon) - $item->weapons[] = [ - 'name' => $localeInfo->weapons->{$weapon->__splatoon3ink_id}?->name ?? $weapon->name, - 'image' => $weapon->image->url, - ]; - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'bigrun', - 'name' => 'Big Run', - 'colour' => 0xFFB322FF, - ]; - - $schedules[] = [ - 'mode' => 'bigrun', - 'schedule' => $schedule, - ]; - } - } - - if(!empty($data->currentFest->id)) { - $currentFest = new stdClass; - $currentFest->id = $data->currentFest->id; - $currentFest->title = $data->currentFest->title; - $currentFest->start = $data->currentFest->startTime; - $currentFest->end = $data->currentFest->endTime; - $currentFest->midterm = $data->currentFest->midtermTime; - $currentFest->state = $data->currentFest->state; - $currentFest->teams = []; - - if($includeFest && !empty($data->currentFest->tricolorStage)) { - $gameModes[] = [ - 'id' => 'tricolor', - 'name' => 'Tricolor Battle', - 'colour' => 0xFF71717A, - ]; - - $schedules[] = [ - 'mode' => 'tricolor', - 'schedule' => [ - [ - 'start' => splatoon2_format_date(strtotime('today')), - 'end' => splatoon2_format_date(strtotime('tomorrow')), - 'variant' => 'vs', - 'ruleset' => [ - 'id' => 'TRICOLOR_TURF_WAR', - 'name' => 'Tricolor Turf War', - 'short' => 'TTW', - ], - 'stages' => [ - [ - 'id' => $data->currentFest->tricolorStage->id, - 'name' => $data->currentFest->tricolorStage->name, - 'image' => $data->currentFest->tricolorStage->image->url, - ], - ], - ], - ], - ]; - } - - foreach($data->currentFest->teams as $rawTeam) { - $currentFest->teams[] = $teamInfo = new stdClass; - $teamInfo->id = $rawTeam->id; - $teamInfo->colour = (round($rawTeam->color->a * 0xFF) << 24) - | (round($rawTeam->color->r * 0xFF) << 16) - | (round($rawTeam->color->g * 0xFF) << 8) - | round($rawTeam->color->b * 0xFF); - } - } - } elseif($gameInfo['variant'] === SP_GAME_SP2) { - $vs = $schedRaw->vs; - $coop = $schedRaw->co; - - $includeRegular = $filterAll || in_array('regular', $filters); - $includeRanked = $filterAll || in_array('gachi', $filters); - $includeLeague = $filterAll || in_array('league', $filters); - $includeSalmon = $filterAll || in_array('coop', $filters); - - function splatoon2_schedule_gen($target, $node): void { - $target->periods = [[ - 'start' => splatoon2_format_date($node->start_time), - 'end' => splatoon2_format_date($node->end_time), - ]]; - } - - function splatoon2_schedule_vs($target, $node): void { - global $localeInfo, $gameModes; - - if(!array_key_exists($node->game_mode->key, $gameModes)) - $gameModes[$node->game_mode->key] = [ - 'id' => $node->game_mode->key, - 'name' => $localeInfo->game_modes->{$node->game_mode->key}?->name ?? $node->game_mode->name, - 'colour' => splatoon2_get_mode_colour($node->game_mode->key), - ]; - - $target->variant = 'vs'; - - $target->ruleset = [ - 'id' => splatoon2_translate_ruleset($node->rule->key), - 'name' => $localeInfo->rules->{$node->rule->key}?->name ?? $node->rule->name, - 'short' => splatoon2_ruleset_short($node->rule->key), - ]; - - $target->stages = []; - foreach(['stage_a', 'stage_b', 'stage_c'] as $stage) { - $stage = $node->{$stage} ?? null; - if(empty($stage)) - continue; - - $target->stages[] = [ - 'id' => $stage->id, - 'name' => $localeInfo->stages->{$stage->id}?->name ?? $stage->name, - 'image' => sprintf(SP2_IMAGE_FORMAT, $stage->image), - ]; - } - } - - if($includeRegular && !empty($vs->regular)) { - $schedule = []; - - foreach($vs->regular as $node) { - if(empty($node->rule)) - continue; - - $schedule[] = $item = new stdClass; - splatoon2_schedule_gen($item, $node); - splatoon2_schedule_vs($item, $node); - } - - if(!empty($schedule)) - $schedules[] = [ - 'mode' => 'regular', - 'schedule' => $schedule, - ]; - } - - if($includeRanked && !empty($vs->gachi)) { - $schedule = []; - - foreach($vs->gachi as $node) { - if(empty($node->rule)) - continue; - - $schedule[] = $item = new stdClass; - splatoon2_schedule_gen($item, $node); - splatoon2_schedule_vs($item, $node); - } - - if(!empty($schedule)) - $schedules[] = [ - 'mode' => 'gachi', - 'schedule' => $schedule, - ]; - } - - if($includeLeague && !empty($vs->league)) { - $schedule = []; - - foreach($vs->league as $node) { - if(empty($node->rule)) - continue; - - $schedule[] = $item = new stdClass; - splatoon2_schedule_gen($item, $node); - splatoon2_schedule_vs($item, $node); - } - - if(!empty($schedule)) - $schedules[] = [ - 'mode' => 'league', - 'schedule' => $schedule, - ]; - } - - if($includeSalmon && !empty($coop->schedules)) { - $schedule = []; - - foreach($coop->schedules as $key => $node) { - $schedule[] = $item = new stdClass; - splatoon2_schedule_gen($item, $node); - $item->variant = 'coop'; - - $details = $coop->details[$key] ?? null; - if(!empty($details)) { - $item->stages = [ - [ - 'name' => $details->stage->name, - 'image' => sprintf(SP2_IMAGE_FORMAT, $details->stage->image), - ], - ]; - - - $item->weapons = []; - foreach($details->weapons as $weapon) { - if($weapon->id === '-1') { - $weaponImage = $weapon->coop_special_weapon->image; - $weaponName = $localeInfo->coop_special_weapons->{$weaponImage}?->name ?? $weapon->coop_special_weapon->name; - } else { - $weaponName = $localeInfo->weapons->{$weapon->weapon->id}?->name ?? $weapon->weapon->name; - $weaponImage = $weapon->weapon->image; - } - - $item->weapons[] = [ - 'id' => $weapon->id, - 'name' => $weaponName, - 'image' => sprintf(SP2_IMAGE_FORMAT, $weaponImage), - ]; - } - } - } - - if(!empty($schedule)) { - $gameModes[] = [ - 'id' => 'coop', - 'name' => 'Salmon Run', - 'colour' => splatoon2_get_mode_colour('coop'), - ]; - - $schedules[] = [ - 'mode' => 'coop', - 'schedule' => $schedule, - ]; - } - } - } else die('{"error":"schedules:variant"}'); - - echo json_encode([ - 'game' => [ - 'id' => $gameId, - 'name' => $gameInfo['name'], - 'colour' => $gameInfo['colour'], - 'variant' => $gameInfo['variant'], - ], - 'fest' => $currentFest, - 'modes' => array_values($gameModes), - 'schedules' => $schedules, - ]); - exit; -} - -if($typeId === SP_TYPE_FESTS) { // SPLATFEST SHIT - $regions = array_filter(explode(',', (string)filter_input(INPUT_GET, 'fr'))); - $regionsAll = empty($regions); - foreach($regions as $key => $region) { - if(!empty($gameInfo['festRegionAliases']) && array_key_exists($region, $gameInfo['festRegionAliases'])) - $region = $gameInfo['festRegionAliases'][$region]; - if(!in_array($region, $gameInfo['festRegions'])) - die('{"error":"fests:region"}'); - $regions[$key] = $region; - } - - if($regionsAll) - $regions = $gameInfo['festRegions']; - - $festsRaw = (object)cached(sprintf('%s-fests', $gameId), 'cache_lifetime_15min', function() use($gameInfo) { - if(empty($gameInfo['festUrl'])) - die('{"error":"fests:url"}'); - - return httpRequest($gameInfo['festUrl']); - }); - - $fests = []; - - if($gameInfo['variant'] === SP_GAME_SP3) { - foreach($regions as $region) { - if(empty($festsRaw->{$region}->data->festRecords->nodes)) - continue; - $nodes = $festsRaw->{$region}->data->festRecords->nodes; - - if(!empty($gameInfo['festRegionAliasesReverse']) && array_key_exists($region, $gameInfo['festRegionAliasesReverse'])) - $region = $gameInfo['festRegionAliasesReverse'][$region]; - - foreach($nodes as $node) { - $festId = $node->image->url; - if(empty($fests[$festId])) - $fests[$festId] = [ - 'regions' => [], - ]; - - $fests[$festId]['regions'][] = $festInfo = new stdClass; - $festInfo->id = $node->id; - $festInfo->region = $region; - $festInfo->state = $node->state; - $festInfo->start = $node->startTime; - $festInfo->end = $node->endTime; - $festInfo->title = $node->title; - $festInfo->image = $node->image->url; - $festInfo->can_vote = $node->isVotable; - $festInfo->teams = []; - - foreach($node->teams as $rawTeam) { - $festInfo->teams[] = $team = new stdClass; - $team->id = $rawTeam->id; - $team->name = $rawTeam->teamName; - $team->image = $rawTeam->image->url; - $team->colour = (round($rawTeam->color->a * 0xFF) << 24) - | (round($rawTeam->color->r * 0xFF) << 16) - | (round($rawTeam->color->g * 0xFF) << 8) - | round($rawTeam->color->b * 0xFF); - - $team->has_results = !empty($rawTeam->result); - if(!$team->has_results) - continue; - - $team->is_winner = $rawTeam->result->isWinner; - $team->results = []; - - foreach(['horagai', 'vote', 'regularContribution', 'challengeContribution', 'tricolorContribution'] as $rateType) { - if(!isset($rawTeam->result->{$rateType . 'Ratio'})) - continue; - - $rateCat = $rateType; - if(str_ends_with($rateCat, 'Contribution')) - $rateCat = substr($rateCat, 0, -12); - - $team->results[] = [ - 'category' => $rateCat, - 'ratio' => $rawTeam->result->{$rateType . 'Ratio'}, - 'is_top' => $rawTeam->result->{'is' . ucfirst($rateType) . 'RatioTop'}, - ]; - } - } - } - } - } elseif($gameInfo['variant'] === SP_GAME_SP2) { - foreach($regions as $region) { - if(empty($festsRaw->{$region}->festivals)) - continue; - $festivals = $festsRaw->{$region}->festivals; - $results = $festsRaw->{$region}->results; - - if(!empty($gameInfo['festRegionAliasesReverse']) && array_key_exists($region, $gameInfo['festRegionAliasesReverse'])) - $region = $gameInfo['festRegionAliasesReverse'][$region]; - - foreach($festivals as $festId => $festRaw) { - if(empty($fests[$festId])) - $fests[$festId] = [ - 'regions' => [], - ]; - - $festNames = $localeInfo->festivals->{$festRaw->festival_id}->names ?? null; - if(empty($festNames)) - continue; - - foreach($results as $findResultRaw) - if($findResultRaw->festival_id === $festRaw->festival_id) { - $resultsRaw = $findResultRaw; - break; - } - - $time = time(); - $state = ''; - if($time > $festRaw->times->end) - $state = 'CLOSED'; - elseif($time > $festRaw->times->start) - $state = 'OPEN'; // this is an assumption - else - $state = 'SCHEDULED'; - - $fests[$festId]['regions'][] = $festInfo = new stdClass; - $festInfo->id = base64_encode(sprintf('FestTeam-%s:JEA-%05d', $region, $festRaw->festival_id)); - $festInfo->region = $region; - $festInfo->state = $state; - $festInfo->start = splatoon2_format_date($festRaw->times->start); - $festInfo->end = splatoon2_format_date($festRaw->times->end); - $festInfo->title = sprintf('%s vs %s', $festNames->alpha_short, $festNames->bravo_short); - $festInfo->image = sprintf(SP2_IMAGE_FORMAT, $festRaw->images->panel); - $festInfo->can_vote = $state !== 'CLOSED'; - $festInfo->teams = []; - $festInfo->special_stages = [ - [ - 'id' => $festRaw->special_stage->id, - 'name' => $localeInfo->stages->{$festRaw->special_stage->id}?->name ?? $festRaw->special_stage->name, - 'image' => sprintf(SP2_IMAGE_FORMAT, $festRaw->special_stage->image), - ], - ]; - - $tmpResults = []; - - foreach(['alpha', 'bravo'] as $teamId => $teamName) { - $festInfo->teams[] = $team = new stdClass; - $team->id = base64_encode(sprintf('FestTeam-%s:JEA-%05d:%s', $region, $festInfo->id, ucfirst($teamName))); - $team->name = $festNames->{$teamName . '_long'}; - $team->image = sprintf(SP2_IMAGE_FORMAT, $festRaw->images->{$teamName}); - - $rawColour = $festRaw->colors->{$teamName}; - $team->colour = (round($rawColour->a * 0xFF) << 24) - | (round($rawColour->r * 0xFF) << 16) - | (round($rawColour->g * 0xFF) << 8) - | round($rawColour->b * 0xFF); - - $team->has_results = !empty($resultsRaw); - if(!$team->has_results) - continue; - - $team->is_winner = $resultsRaw->summary->total === $teamId; - $team->results = []; - foreach(['challenge', 'vote', 'regular', 'solo', 'team'] as $rateType) { - $rawRate = $resultsRaw->rates->{$rateType}->{$teamName} ?? null; - if(empty($rawRate)) - continue; - - if(!isset($tmpResults[$rateType])) - $tmpResults[$rateType] = $rawRate > $resultsRaw->rates->{$rateType}->{'bravo'} ?? 0; - - $team->results[] = [ - 'category' => $rateType, - 'ratio' => $rawRate / 10000, - 'is_top' => $tmpResults[$rateType], - ]; - - $tmpResults[$rateType] = !$tmpResults[$rateType]; - } - } - - unset($tmpResults); - } - } - } else die('{"error":"fests:variant"}'); - - echo json_encode([ - 'game' => [ - 'id' => $gameId, - 'name' => $gameInfo['name'], - 'colour' => $gameInfo['colour'], - 'variant' => $gameInfo['variant'], - ], - 'fests' => array_values($fests), - ]); - exit; -} diff --git a/public/translate.php b/public/translate.php deleted file mode 100644 index a2f6d90..0000000 --- a/public/translate.php +++ /dev/null @@ -1,150 +0,0 @@ -\r\n"; - - switch ($mode) { - case 'list': - $type = get_class($arr[0]); - $output .= ""; - foreach ($arr as $part) { - $output .= "<{$type} "; - - foreach (get_object_vars($part) as $name => $value) { - $output .= "{$name}=\"{$value}\" "; - } - - $output .= '/>'; - } - $output .= ""; - break; - - case 'do': - $output .= " $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); -} diff --git a/public/word-define.php b/public/word-define.php deleted file mode 100644 index a336938..0000000 --- a/public/word-define.php +++ /dev/null @@ -1,39 +0,0 @@ -text)) - continue; - - $defs[] = [ - 'word' => $def->word ?? '', - 'pos' => $def->partOfSpeech ?? '', - 'attr' => $def->attributionText ?? '', - 'attr_url' => $def->attributionUrl ?? '', - 'dict' => $def->sourceDictionary ?? '', - 'text' => strtr($def->text, [ - '' => '[i]', - '' => '[/i]', - ]), - 'url' => $def->wordnikUrl ?? '', - ]; -} - -echo json_encode($defs); diff --git a/satori.php b/satori.php new file mode 100644 index 0000000..8180b6e --- /dev/null +++ b/satori.php @@ -0,0 +1,25 @@ +getString('database:dsn', 'null')); +$dbc->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';'); + +$sat = new SatoriContext($cfg, $dbc); diff --git a/src/Booru/BooruContext.php b/src/Booru/BooruContext.php new file mode 100644 index 0000000..a4b5157 --- /dev/null +++ b/src/Booru/BooruContext.php @@ -0,0 +1,30 @@ +sources[] = new Danbooru\DanbooruSource($config->scopeTo('danbooru')); + $this->sources[] = new Gelbooru\GelbooruSource($config->scopeTo('gelbooru')); + $this->sources[] = new Konachan\KonachanSource($config->scopeTo('konachan')); + $this->sources[] = new Yandere\YandereSource($config->scopeTo('yandere')); + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getSources(): array { + return $this->sources; + } + + public function getSourceByName(string $name): ?IBooruSource { + foreach($this->sources as $source) + if($source->getInfo()->getName() === $name) + return $source; + return null; + } +} diff --git a/src/Booru/BooruErrorResponse.php b/src/Booru/BooruErrorResponse.php new file mode 100644 index 0000000..882bea6 --- /dev/null +++ b/src/Booru/BooruErrorResponse.php @@ -0,0 +1,16 @@ +code; + } + + public function jsonSerialize(): mixed { + return ['error' => $this->code]; + } +} diff --git a/src/Booru/BooruQueryResponse.php b/src/Booru/BooruQueryResponse.php new file mode 100644 index 0000000..42120ec --- /dev/null +++ b/src/Booru/BooruQueryResponse.php @@ -0,0 +1,24 @@ +source; + } + + public function getPosts(): array { + return $this->posts; + } + + public function jsonSerialize(): mixed { + return [ + 'source' => $this->source, + 'posts' => $this->posts, + ]; + } +} diff --git a/src/Booru/BooruRoutes.php b/src/Booru/BooruRoutes.php new file mode 100644 index 0000000..b6c87c1 --- /dev/null +++ b/src/Booru/BooruRoutes.php @@ -0,0 +1,60 @@ +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)) + ); + } +} diff --git a/src/Booru/BooruSourceInfo.php b/src/Booru/BooruSourceInfo.php new file mode 100644 index 0000000..9f8cfdb --- /dev/null +++ b/src/Booru/BooruSourceInfo.php @@ -0,0 +1,25 @@ +name; + } + + public function getTitle(): string { + return $this->title; + } + + public function jsonSerialize(): mixed { + return [ + 'name' => $this->name, + 'type' => $this->name, + 'title' => $this->title, + ]; + } +} diff --git a/src/Booru/Danbooru/DanbooruPost.php b/src/Booru/Danbooru/DanbooruPost.php new file mode 100644 index 0000000..86704e9 --- /dev/null +++ b/src/Booru/Danbooru/DanbooruPost.php @@ -0,0 +1,52 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating']; + } + + public function getPostUrl(): string { + return sprintf('https://danbooru.donmai.us/posts/%d', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tag_string']); + } + + public function hasFileUrl(): bool { + return array_key_exists('file_url', $this->info); + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } + + public function getGeneralTags(): array { + return explode(' ', $this->info['tag_string_general']); + } + + public function getCharacterTags(): array { + return explode(' ', $this->info['tag_string_character']); + } + + public function getCopyrightTags(): array { + return explode(' ', $this->info['tag_string_copyright']); + } + + public function getArtistTags(): array { + return explode(' ', $this->info['tag_string_artist']); + } + + public function getMetaTags(): array { + return explode(' ', $this->info['tag_string_meta']); + } +} diff --git a/src/Booru/Danbooru/DanbooruSource.php b/src/Booru/Danbooru/DanbooruSource.php new file mode 100644 index 0000000..e34d236 --- /dev/null +++ b/src/Booru/Danbooru/DanbooruSource.php @@ -0,0 +1,62 @@ +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)); + } +} diff --git a/src/Booru/Gelbooru/GelbooruPost.php b/src/Booru/Gelbooru/GelbooruPost.php new file mode 100644 index 0000000..2267afe --- /dev/null +++ b/src/Booru/Gelbooru/GelbooruPost.php @@ -0,0 +1,32 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating'][0]; + } + + public function getPostUrl(): string { + return sprintf('https://gelbooru.com/index.php?page=post&s=view&id=%d', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tags']); + } + + public function hasFileUrl(): bool { + return false; // remote embedding is blocked + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } +} diff --git a/src/Booru/Gelbooru/GelbooruSource.php b/src/Booru/Gelbooru/GelbooruSource.php new file mode 100644 index 0000000..fdb3410 --- /dev/null +++ b/src/Booru/Gelbooru/GelbooruSource.php @@ -0,0 +1,59 @@ +info = new BooruSourceInfo('gelbooru', 'Gelbooru'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + if($random) + $tags .= ' sort:random'; + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + return empty($response) || empty($response['post']) || !is_array($response['post']) + ? [] : XArray::select($response['post'], fn($post) => new GelbooruPost($post)); + } +} diff --git a/src/Booru/IBooruPost.php b/src/Booru/IBooruPost.php new file mode 100644 index 0000000..74b5be1 --- /dev/null +++ b/src/Booru/IBooruPost.php @@ -0,0 +1,12 @@ +info['id']; + } + + public function getRating(): string { + return $this->info['rating']; + } + + public function isSafe(): bool { + return $this->info['rating'] === 'g' + || $this->info['rating'] === 's'; + } + + public function getPostUrl(): string { + return sprintf('https://konachan.%s/post/show/%d', $this->isSafe() ? 'net' : 'com', $this->info['id']); + } + + public function getTags(): array { + return explode(' ', $this->info['tags']); + } + + public function hasFileUrl(): bool { + return false; // remote embedding is blocked + } + + public function getFileUrl(): string { + return $this->info['file_url']; + } +} diff --git a/src/Booru/Konachan/KonachanSource.php b/src/Booru/Konachan/KonachanSource.php new file mode 100644 index 0000000..6a3b429 --- /dev/null +++ b/src/Booru/Konachan/KonachanSource.php @@ -0,0 +1,59 @@ +info = new BooruSourceInfo('konachan', 'Konachan'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + if($random) + $tags .= ' order:random'; + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + return empty($response) || empty($response['posts']) || !is_array($response['posts']) + ? [] : XArray::select($response['posts'], fn($post) => new KonachanPost($post)); + } +} diff --git a/src/Booru/Yandere/YanderePost.php b/src/Booru/Yandere/YanderePost.php new file mode 100644 index 0000000..7f488ae --- /dev/null +++ b/src/Booru/Yandere/YanderePost.php @@ -0,0 +1,32 @@ +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']; + } +} diff --git a/src/Booru/Yandere/YandereSource.php b/src/Booru/Yandere/YandereSource.php new file mode 100644 index 0000000..4d17df1 --- /dev/null +++ b/src/Booru/Yandere/YandereSource.php @@ -0,0 +1,78 @@ +info = new BooruSourceInfo('yandere', 'Yandere'); + } + + public function getInfo(): BooruSourceInfo { + return $this->info; + } + + public function getPosts(string $tags, bool $random = true, int $limit = 20): array { + $localRandom = false; + $localLimit = $limit; + + if($random) { + $localRandom = trim($tags) !== ''; + + if($localRandom) + $limit = max($limit, 200); + else + $tags = 'order:random'; + } + + $params = []; + if($tags !== '') + $params['tags'] = $tags; + if($limit > 0) + $params['limit'] = $limit; + + $url = self::POSTS_URL; + if(!empty($params)) + $url .= '&' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => sprintf('Satori/%s (+https://fii.moe/satori)', SAT_VERSION), + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl), true); + curl_close($curl); + + if(empty($response) || empty($response['posts']) || !is_array($response['posts'])) + return []; + + $posts = $response['posts']; + + if($localRandom) { + shuffle($posts); + if($localLimit > 0) + $posts = array_slice($posts, 0, $localLimit); + } + + return XArray::select($posts, fn($post) => new YanderePost($post)); + } +} diff --git a/src/DatabaseContext.php b/src/DatabaseContext.php new file mode 100644 index 0000000..b6113e2 --- /dev/null +++ b/src/DatabaseContext.php @@ -0,0 +1,30 @@ +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); + } +} diff --git a/src/Dictionary/DictionaryContext.php b/src/Dictionary/DictionaryContext.php new file mode 100644 index 0000000..1ac45e6 --- /dev/null +++ b/src/Dictionary/DictionaryContext.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/src/Dictionary/DictionaryResult.php b/src/Dictionary/DictionaryResult.php new file mode 100644 index 0000000..e0272cf --- /dev/null +++ b/src/Dictionary/DictionaryResult.php @@ -0,0 +1,42 @@ +info['word'] ?? ''; + } + + public function getPartOfSpeech(): string { + return $this->info['partOfSpeech'] ?? ''; + } + + public function getAttributionText(): string { + return $this->info['attributionText'] ?? ''; + } + + public function getAttributionUrl(): string { + return $this->info['attributionUrl'] ?? ''; + } + + public function getSourceDictionary(): string { + return $this->info['sourceDictionary'] ?? ''; + } + + public function getWordnikUrl(): string { + return $this->info['wordnikUrl'] ?? ''; + } + + public function hasText(): bool { + return array_key_exists('text', $this->info) + && $this->info['text'] !== ''; + } + + public function getText(): string { + return strtr($this->info['text'] ?? '', [ + '' => '[i]', + '' => '[/i]', + ]); + } +} diff --git a/src/Dictionary/DictionaryRoutes.php b/src/Dictionary/DictionaryRoutes.php new file mode 100644 index 0000000..20556e0 --- /dev/null +++ b/src/Dictionary/DictionaryRoutes.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/src/ExRate/ExRateContext.php b/src/ExRate/ExRateContext.php new file mode 100644 index 0000000..3b64ee5 --- /dev/null +++ b/src/ExRate/ExRateContext.php @@ -0,0 +1,68 @@ +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); + } +} diff --git a/src/ExRate/ExRateDatabase.php b/src/ExRate/ExRateDatabase.php new file mode 100644 index 0000000..9dd3ee1 --- /dev/null +++ b/src/ExRate/ExRateDatabase.php @@ -0,0 +1,49 @@ +cache = new DbStatementCache($dbConn); + } + + public function ratesNeedRefresh(): bool { + $result = $this->dbConn->query('SELECT MAX(rate_stored) > NOW() - INTERVAL 1 DAY FROM `exchange-rates` LIMIT 1'); + return !$result->next() || $result->isNull(0) || $result->getInteger(0) < 1; + } + + public function clearRates(): void { + $this->dbConn->execute('TRUNCATE `exchange-rates`'); + } + + public function insertRate(string $to, float $rate): void { + $stmt = $this->cache->get(sprintf( + 'INSERT INTO `exchange-rates` (rate_from, rate_to, rate_value) VALUES ("%s", ?, ?)', + $this->baseCurrency + )); + $stmt->addParameter(1, $to); + $stmt->addParameter(2, $rate); + $stmt->execute(); + } + + public function convertRate(string $from, string $to, float $value): float { + $stmt = $this->cache->get(sprintf( + 'SELECT (SELECT (? / rate_value) FROM `exchange-rates` WHERE rate_from = "%1$s" AND rate_to = ?) * rate_value FROM `exchange-rates` WHERE rate_from = "%1$s" AND rate_to = ?', + $this->baseCurrency + )); + $stmt->addParameter(1, $value); + $stmt->addParameter(2, $from); + $stmt->addParameter(3, $to); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? $result->getFloat(0) : 0; + } +} diff --git a/src/ExRate/ExRateRoutes.php b/src/ExRate/ExRateRoutes.php new file mode 100644 index 0000000..daa64e4 --- /dev/null +++ b/src/ExRate/ExRateRoutes.php @@ -0,0 +1,93 @@ +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; + } +} diff --git a/src/RoutingContext.php b/src/RoutingContext.php new file mode 100644 index 0000000..415e631 --- /dev/null +++ b/src/RoutingContext.php @@ -0,0 +1,36 @@ +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); + } +} diff --git a/src/SFileCache.php b/src/SFileCache.php new file mode 100644 index 0000000..945e2df --- /dev/null +++ b/src/SFileCache.php @@ -0,0 +1,23 @@ + is_int($mtime) && floor($mtime / 900) >= floor(time() / 900), $create); + } +} diff --git a/src/SHttp.php b/src/SHttp.php new file mode 100644 index 0000000..10ec289 --- /dev/null +++ b/src/SHttp.php @@ -0,0 +1,32 @@ + 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)); + } +} diff --git a/src/SatoriContext.php b/src/SatoriContext.php new file mode 100644 index 0000000..602c853 --- /dev/null +++ b/src/SatoriContext.php @@ -0,0 +1,67 @@ +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; + } +} diff --git a/src/Splatoon/ISplatoonFestival.php b/src/Splatoon/ISplatoonFestival.php new file mode 100644 index 0000000..ae39484 --- /dev/null +++ b/src/Splatoon/ISplatoonFestival.php @@ -0,0 +1,20 @@ +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']); + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2Festival.php b/src/Splatoon/Splatoon2/Splatoon2Festival.php new file mode 100644 index 0000000..0eb43d7 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2Festival.php @@ -0,0 +1,122 @@ +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'])]; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2FestivalTeam.php b/src/Splatoon/Splatoon2/Splatoon2FestivalTeam.php new file mode 100644 index 0000000..aac8400 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2FestivalTeam.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2FestivalTeamResult.php b/src/Splatoon/Splatoon2/Splatoon2FestivalTeamResult.php new file mode 100644 index 0000000..98b4463 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2FestivalTeamResult.php @@ -0,0 +1,24 @@ +category; + } + + public function getRatio(): float { + return $this->ratio; + } + + public function isTop(): bool { + return $this->isTop; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2Game.php b/src/Splatoon/Splatoon2/Splatoon2Game.php new file mode 100644 index 0000000..115c0ef --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2Game.php @@ -0,0 +1,243 @@ + '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; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2GameMode.php b/src/Splatoon/Splatoon2/Splatoon2GameMode.php new file mode 100644 index 0000000..d05e205 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2GameMode.php @@ -0,0 +1,36 @@ +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 []; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2Locale.php b/src/Splatoon/Splatoon2/Splatoon2Locale.php new file mode 100644 index 0000000..eeb63d7 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2Locale.php @@ -0,0 +1,42 @@ + 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; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryCoop.php b/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryCoop.php new file mode 100644 index 0000000..7f28782 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryCoop.php @@ -0,0 +1,42 @@ +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)); + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryVs.php b/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryVs.php new file mode 100644 index 0000000..b5f88ca --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2ScheduleEntryVs.php @@ -0,0 +1,43 @@ +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']); + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2Stage.php b/src/Splatoon/Splatoon2/Splatoon2Stage.php new file mode 100644 index 0000000..beab9ce --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2Stage.php @@ -0,0 +1,26 @@ +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']); + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2TimePeriod.php b/src/Splatoon/Splatoon2/Splatoon2TimePeriod.php new file mode 100644 index 0000000..f92b189 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2TimePeriod.php @@ -0,0 +1,19 @@ +startTime; + } + + public function getEndTime(): int { + return $this->endTime; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2VsRuleset.php b/src/Splatoon/Splatoon2/Splatoon2VsRuleset.php new file mode 100644 index 0000000..e69b781 --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2VsRuleset.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/src/Splatoon/Splatoon2/Splatoon2Weapon.php b/src/Splatoon/Splatoon2/Splatoon2Weapon.php new file mode 100644 index 0000000..954e24b --- /dev/null +++ b/src/Splatoon/Splatoon2/Splatoon2Weapon.php @@ -0,0 +1,26 @@ +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']; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3Festival.php b/src/Splatoon/Splatoon3/Splatoon3Festival.php new file mode 100644 index 0000000..a3bc973 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3Festival.php @@ -0,0 +1,76 @@ +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 []; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3FestivalTeam.php b/src/Splatoon/Splatoon3/Splatoon3FestivalTeam.php new file mode 100644 index 0000000..219264b --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3FestivalTeam.php @@ -0,0 +1,71 @@ +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; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3FestivalTeamResult.php b/src/Splatoon/Splatoon3/Splatoon3FestivalTeamResult.php new file mode 100644 index 0000000..373146d --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3FestivalTeamResult.php @@ -0,0 +1,24 @@ +category; + } + + public function getRatio(): float { + return $this->ratio; + } + + public function isTop(): bool { + return $this->isTop; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3Game.php b/src/Splatoon/Splatoon3/Splatoon3Game.php new file mode 100644 index 0000000..72130a7 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3Game.php @@ -0,0 +1,342 @@ + '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; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3GameMode.php b/src/Splatoon/Splatoon3/Splatoon3GameMode.php new file mode 100644 index 0000000..d3d5c78 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3GameMode.php @@ -0,0 +1,33 @@ +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; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3GameModeVariant.php b/src/Splatoon/Splatoon3/Splatoon3GameModeVariant.php new file mode 100644 index 0000000..57b48f7 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3GameModeVariant.php @@ -0,0 +1,24 @@ +name; + } + + public function getTitle(): string { + return $this->title; + } + + public function getColour(): int { + return $this->colour; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3League.php b/src/Splatoon/Splatoon3/Splatoon3League.php new file mode 100644 index 0000000..85a6a64 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3League.php @@ -0,0 +1,40 @@ +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('
', "\n", $this->locale->getString( + sprintf('events:%s:desc', $this->eventInfo['id']), + $this->eventInfo['desc'] + )); + } + + public function getRulesLines(): array { + return explode("\n", str_replace('
', "\n", $this->locale->getString( + sprintf('events:%s:regulation', $this->eventInfo['id']), + $this->eventInfo['regulation'] + ))); + } + + public function getRulesUrl(): string { + return $this->eventInfo['regulationUrl'] ?? ''; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3Locale.php b/src/Splatoon/Splatoon3/Splatoon3Locale.php new file mode 100644 index 0000000..73d17aa --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3Locale.php @@ -0,0 +1,64 @@ + 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('
', "\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; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryCoop.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryCoop.php new file mode 100644 index 0000000..acd662d --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryCoop.php @@ -0,0 +1,37 @@ +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)); + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVs.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVs.php new file mode 100644 index 0000000..3b36ea0 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVs.php @@ -0,0 +1,38 @@ +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']); + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsLeague.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsLeague.php new file mode 100644 index 0000000..8c09a01 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsLeague.php @@ -0,0 +1,38 @@ +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']); + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsTricolor.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsTricolor.php new file mode 100644 index 0000000..1f4eed3 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleEntryVsTricolor.php @@ -0,0 +1,36 @@ +locale, $this->tricolorStage), + ]; + } + + public function getRuleset(): ISplatoonVsRuleset { + return new Splatoon3VsRulesetTricolor; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleFestival.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleFestival.php new file mode 100644 index 0000000..6ed3c17 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleFestival.php @@ -0,0 +1,40 @@ +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)); + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3ScheduleFestivalTeam.php b/src/Splatoon/Splatoon3/Splatoon3ScheduleFestivalTeam.php new file mode 100644 index 0000000..05186a5 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3ScheduleFestivalTeam.php @@ -0,0 +1,21 @@ +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); + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3Stage.php b/src/Splatoon/Splatoon3/Splatoon3Stage.php new file mode 100644 index 0000000..07c2b60 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3Stage.php @@ -0,0 +1,27 @@ +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']; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3TimePeriod.php b/src/Splatoon/Splatoon3/Splatoon3TimePeriod.php new file mode 100644 index 0000000..6d52675 --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3TimePeriod.php @@ -0,0 +1,22 @@ +startTime = strtotime($startTime); + $this->endTime = strtotime($endTime); + } + + public function getStartTime(): int { + return $this->startTime; + } + + public function getEndTime(): int { + return $this->endTime; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3VsRuleset.php b/src/Splatoon/Splatoon3/Splatoon3VsRuleset.php new file mode 100644 index 0000000..58d30de --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3VsRuleset.php @@ -0,0 +1,39 @@ +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; + } +} diff --git a/src/Splatoon/Splatoon3/Splatoon3VsRulesetTricolor.php b/src/Splatoon/Splatoon3/Splatoon3VsRulesetTricolor.php new file mode 100644 index 0000000..e9b428b --- /dev/null +++ b/src/Splatoon/Splatoon3/Splatoon3VsRulesetTricolor.php @@ -0,0 +1,18 @@ +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']; + } +} diff --git a/src/Splatoon/SplatoonContext.php b/src/Splatoon/SplatoonContext.php new file mode 100644 index 0000000..8c76e3e --- /dev/null +++ b/src/Splatoon/SplatoonContext.php @@ -0,0 +1,28 @@ +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; + } +} diff --git a/src/Splatoon/SplatoonErrorResponse.php b/src/Splatoon/SplatoonErrorResponse.php new file mode 100644 index 0000000..336615b --- /dev/null +++ b/src/Splatoon/SplatoonErrorResponse.php @@ -0,0 +1,16 @@ +code; + } + + public function jsonSerialize(): mixed { + return ['error' => $this->code]; + } +} diff --git a/src/Splatoon/SplatoonRoutes.php b/src/Splatoon/SplatoonRoutes.php new file mode 100644 index 0000000..810a026 --- /dev/null +++ b/src/Splatoon/SplatoonRoutes.php @@ -0,0 +1,241 @@ + $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'); + } +} diff --git a/src/Translation/TranslateContext.php b/src/Translation/TranslateContext.php new file mode 100644 index 0000000..4cda628 --- /dev/null +++ b/src/Translation/TranslateContext.php @@ -0,0 +1,85 @@ +config = $config; + } + + public function getConfig(): IConfig { + return $this->config; + } + + public function getGoogleLanguageCodes(): array { + return self::GOOGLE_LANGS; + } + + public function getGoogleLanguages(): array { + $langs = []; + + foreach(self::GOOGLE_LANGS as $langId) + $langs[] = [ + 'code' => $langId, + 'name' => locale_get_display_name($langId), + ]; + + return $langs; + } + + public function googleTranslate(string $from, string $to, string $text): ?object { + $curl = curl_init(sprintf( + self::GOOGLE_TRANSLATE, + rawurlencode($from), + rawurlencode($to), + rawurlencode($text) + )); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => self::GOOGLE_USERAGENT, + ]); + $response = curl_exec($curl); + curl_close($curl); + + if(empty($response)) + return null; + + $response = json_decode($response); + if(empty($response)) + return null; + + $result = new stdClass; + $result->translatedText = $response[0][0][0] ?? ''; + $result->originalText = $response[0][0][1] ?? ''; + $result->fromLangCode = $response[2] ?? ''; + $result->fromLangName = locale_get_display_name($result->fromLangCode); + $result->toLangCode = $to; + $result->toLangName = locale_get_display_name($to); + + return $result; + } +} diff --git a/src/Translation/TranslateRoutes.php b/src/Translation/TranslateRoutes.php new file mode 100644 index 0000000..2c48167 --- /dev/null +++ b/src/Translation/TranslateRoutes.php @@ -0,0 +1,99 @@ +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 ''; + } +} diff --git a/tools/migrate b/tools/migrate new file mode 100755 index 0000000..09eaacf --- /dev/null +++ b/tools/migrate @@ -0,0 +1,34 @@ +#!/usr/bin/env php +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'); +} diff --git a/tools/new-migration b/tools/new-migration new file mode 100755 index 0000000..c30ca8d --- /dev/null +++ b/tools/new-migration @@ -0,0 +1,26 @@ +#!/usr/bin/env php +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;