diff --git a/CHANGELOG.md b/CHANGELOG.md index f5dfef3..b9c0682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- [Profile Information](https://wiki.vg/Mojang_API#Profile_Information) endpoint. ## [0.2.1] - 2020-06-10 ### Added diff --git a/src/Api.php b/src/Api.php index 30ba9fd..b863a34 100644 --- a/src/Api.php +++ b/src/Api.php @@ -13,6 +13,7 @@ use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\RequestOptions; use InvalidArgumentException; use Ramsey\Uuid\Uuid; @@ -524,7 +525,7 @@ class Api { * * @url https://wiki.vg/Mojang_API#Statistics */ - public function statistics(array $metricKeys) { + public function statistics(array $metricKeys) { // TODO: missing return type annotation $response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [ 'json' => [ 'metricKeys' => $metricKeys, @@ -535,6 +536,43 @@ class Api { return new Response\StatisticsResponse($body['total'], $body['last24h'], $body['saleVelocityPerSeconds']); } + /** + * @param string $accessToken + * @return \Ely\Mojang\Response\MinecraftServicesProfileResponse + * @throws GuzzleException + * @see https://wiki.vg/Mojang_API#Profile_Information + */ + public function getProfile(string $accessToken): Response\MinecraftServicesProfileResponse { + $response = $this->getClient()->request('GET', 'https://api.minecraftservices.com/minecraft/profile', [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Bearer ' . $accessToken, + ], + ]); + $body = $this->decode($response->getBody()->getContents()); + + return new Response\MinecraftServicesProfileResponse( + $body['id'], + $body['name'], + array_map(function(array $item) { + return new Response\MinecraftServicesProfileSkin( + $item['id'], + $item['state'], + $item['url'], + $item['variant'], + $item['alias'] ?? null + ); + }, $body['skins']), + array_map(function(array $item) { + return new Response\MinecraftServicesProfileCape( + $item['id'], + $item['state'], + $item['url'], + $item['alias'] + ); + }, $body['capes']) + ); + } + /** * @return ClientInterface */ diff --git a/src/Response/MinecraftServicesProfileCape.php b/src/Response/MinecraftServicesProfileCape.php new file mode 100644 index 0000000..915bb80 --- /dev/null +++ b/src/Response/MinecraftServicesProfileCape.php @@ -0,0 +1,58 @@ +id = $id; + $this->state = $state; + $this->url = $url; + $this->alias = $alias; + } + + public function getId(): string { + return $this->id; + } + + /** + * TODO: figure out literal for not active state + * @return 'ACTIVE' + */ + public function getState(): string { + return $this->state; + } + + public function getUrl(): string { + return $this->url; + } + + public function getAlias(): string { + return $this->alias; + } + +} diff --git a/src/Response/MinecraftServicesProfileResponse.php b/src/Response/MinecraftServicesProfileResponse.php new file mode 100644 index 0000000..a12343d --- /dev/null +++ b/src/Response/MinecraftServicesProfileResponse.php @@ -0,0 +1,66 @@ +id = $id; + $this->name = $name; + $this->skins = $skins; + $this->capes = $capes; + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + /** + * @return MinecraftServicesProfileSkin[] + */ + public function getSkins(): array { + return $this->skins; + } + + /** + * @return MinecraftServicesProfileCape[] + */ + public function getCapes(): array { + return $this->capes; + } + +} diff --git a/src/Response/MinecraftServicesProfileSkin.php b/src/Response/MinecraftServicesProfileSkin.php new file mode 100644 index 0000000..7de6a8c --- /dev/null +++ b/src/Response/MinecraftServicesProfileSkin.php @@ -0,0 +1,74 @@ +id = $id; + $this->state = $state; + $this->url = $url; + $this->variant = $variant; + $this->alias = $alias; + } + + public function getId(): string { + return $this->id; + } + + /** + * TODO: figure out literal for not active state + * @return 'ACTIVE' + */ + public function getState(): string { + return $this->state; + } + + public function getUrl(): string { + return $this->url; + } + + /** + * @return 'CLASSIC'|'SLIM' + */ + public function getVariant(): string { + return $this->variant; + } + + /** + * Doesn't show up for some reason for some accounts + */ + public function getAlias(): ?string { + return $this->alias; + } + +} diff --git a/tests/ApiTest.php b/tests/ApiTest.php index e0c6f4e..0422b07 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -735,6 +735,49 @@ class ApiTest extends TestCase { $this->assertSame(1.32, $result->getSaleVelocityPerSeconds()); } + public function testGetProfile(): void { + $this->mockHandler->append($this->createResponse(200, [ + 'id' => '86f6e3695b764412a29820cac1d4d0d6', + 'name' => 'MockUsername', + 'skins' => [ + [ + 'id' => 'b647c354-16b0-47e3-8340-5d328b4215c1', + 'state' => 'ACTIVE', + 'url' => 'http://textures.minecraft.net/texture/3b60a1f6d562f52aaebbf1434f1de147933a3affe0e764fa49ea057536623cd3', + 'variant' => 'SLIM', + ], + ], + 'capes' => [ + [ + 'id' => '402d2b1a-48a5-4177-8173-85b0d1d04889', + 'state' => 'ACTIVE', + 'url' => 'http://textures.minecraft.net/texture/153b1a0dfcbae953cdeb6f2c2bf6bf79943239b1372780da44bcbb29273131da', + 'alias' => 'Minecon2013', + ], + ], + ])); + + $result = $this->api->getProfile('mock access token'); + + /** @var \Psr\Http\Message\RequestInterface $request */ + $request = $this->history[0]['request']; + $this->assertSame('Bearer mock access token', $request->getHeaderLine('Authorization')); + + $this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getId()); + $this->assertSame('MockUsername', $result->getName()); + $this->assertCount(1, $result->getSkins()); + $this->assertSame('b647c354-16b0-47e3-8340-5d328b4215c1', $result->getSkins()[0]->getId()); + $this->assertSame('ACTIVE', $result->getSkins()[0]->getState()); + $this->assertSame('http://textures.minecraft.net/texture/3b60a1f6d562f52aaebbf1434f1de147933a3affe0e764fa49ea057536623cd3', $result->getSkins()[0]->getUrl()); + $this->assertSame('SLIM', $result->getSkins()[0]->getVariant()); + $this->assertNull($result->getSkins()[0]->getAlias()); + $this->assertCount(1, $result->getCapes()); + $this->assertSame('402d2b1a-48a5-4177-8173-85b0d1d04889', $result->getCapes()[0]->getId()); + $this->assertSame('ACTIVE', $result->getCapes()[0]->getState()); + $this->assertSame('http://textures.minecraft.net/texture/153b1a0dfcbae953cdeb6f2c2bf6bf79943239b1372780da44bcbb29273131da', $result->getCapes()[0]->getUrl()); + $this->assertSame('Minecon2013', $result->getCapes()[0]->getAlias()); + } + private function createResponse(int $statusCode, array $response): ResponseInterface { return new Response($statusCode, ['content-type' => 'json'], json_encode($response)); }