diff --git a/CHANGELOG.md b/CHANGELOG.md index 398bb59..80c569b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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 +- [Check if security questions are needed](https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed) endpoint. +- [Get list of questions](https://wiki.vg/Mojang_API#Get_list_of_questions) endpoint. +- [Send back the answers](https://wiki.vg/Mojang_API#Send_back_the_answers) endpoint. +- [Statistics](https://wiki.vg/Mojang_API#Statistics) endpoint. + +### Changed +- Changed the threshold value for [Playernames -> UUIDs](https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs) endpoint + from `100` to `10`. ## [0.2.0] - 2019-05-07 ### Added diff --git a/src/Api.php b/src/Api.php index 55c3eda..30ba9fd 100644 --- a/src/Api.php +++ b/src/Api.php @@ -6,6 +6,7 @@ namespace Ely\Mojang; use DateTime; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; +use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; @@ -150,8 +151,8 @@ class Api { } } - if (count($names) > 100) { - throw new InvalidArgumentException('You cannot request more than 100 names per request'); + if (count($names) > 10) { + throw new InvalidArgumentException('You cannot request more than 10 names per request'); } $response = $this->getClient()->request('POST', 'https://api.mojang.com/profiles/minecraft', [ @@ -452,6 +453,88 @@ class Api { return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']); } + /** + * @param string $accessToken + * @throws GuzzleException + * + * @url https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed + */ + public function isSecurityQuestionsNeeded(string $accessToken): void { + $request = new Request( + 'GET', + 'https://api.mojang.com/user/security/location', + ['Authorization' => 'Bearer ' . $accessToken] + ); + $response = $this->getClient()->send($request); + $rawBody = $response->getBody()->getContents(); + if (!empty($rawBody)) { + $body = $this->decode($rawBody); + throw new Exception\OperationException($body['errorMessage'], $request, $response); + } + } + + /** + * @param string $accessToken + * @return array + * @throws GuzzleException + * + * @url https://wiki.vg/Mojang_API#Get_list_of_questions + */ + public function questions(string $accessToken): array { + $request = new Request( + 'GET', + 'https://api.mojang.com/user/security/challenges', + ['Authorization' => 'Bearer ' . $accessToken] + ); + $response = $this->getClient()->send($request); + $result = []; + $body = $this->decode($response->getBody()->getContents()); + foreach ($body as $question) { + $result[] = new QuestionResponse($question['question']['id'], $question['question']['question'], $question['answer']['id']); + } + + return $result; + } + + /** + * @param string $accessToken + * @param array $answers + * @throws GuzzleException + * @return bool + * + * @url https://wiki.vg/Mojang_API#Send_back_the_answers + */ + public function answer(string $accessToken, array $answers): bool { + $request = new Request( + 'POST', + 'https://api.mojang.com/user/security/location', + ['Authorization' => 'Bearer ' . $accessToken], + json_encode($answers) + ); + $response = $this->getClient()->send($request); + $rawBody = $response->getBody()->getContents(); + + return empty($rawBody); + } + + /** + * @param array $metricKeys + * @return Response\StatisticsResponse + * @throws GuzzleException + * + * @url https://wiki.vg/Mojang_API#Statistics + */ + public function statistics(array $metricKeys) { + $response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [ + 'json' => [ + 'metricKeys' => $metricKeys, + ], + ]); + $body = $this->decode($response->getBody()->getContents()); + + return new Response\StatisticsResponse($body['total'], $body['last24h'], $body['saleVelocityPerSeconds']); + } + /** * @return ClientInterface */ diff --git a/src/Exception/OperationException.php b/src/Exception/OperationException.php new file mode 100644 index 0000000..b791826 --- /dev/null +++ b/src/Exception/OperationException.php @@ -0,0 +1,16 @@ +questionId = $questionId; + $this->question = $question; + $this->answerId = $answerId; + } + + public function getQuestionId(): int { + return $this->questionId; + } + + public function getQuestion(): string { + return $this->question; + } + + public function getAnswerId(): int { + return $this->answerId; + } + +} diff --git a/src/Response/StatisticsResponse.php b/src/Response/StatisticsResponse.php new file mode 100644 index 0000000..5f3d06e --- /dev/null +++ b/src/Response/StatisticsResponse.php @@ -0,0 +1,41 @@ +total = $total; + $this->last24h = $last24h; + $this->saleVelocityPerSeconds = $saleVelocityPerSeconds; + } + + public function getTotal(): int { + return $this->total; + } + + public function getLast24H(): int { + return $this->last24h; + } + + public function getSaleVelocityPerSeconds(): float { + return $this->saleVelocityPerSeconds; + } + +} diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 3b23e23..e0c6f4e 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -6,12 +6,14 @@ namespace Ely\Mojang\Test; use Ely\Mojang\Api; use Ely\Mojang\Exception\ForbiddenException; use Ely\Mojang\Exception\NoContentException; +use Ely\Mojang\Exception\OperationException; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; use Ely\Mojang\Response\ApiStatus; use Ely\Mojang\Response\NameHistoryItem; use Ely\Mojang\Response\ProfileInfo; use Ely\Mojang\Response\Properties\TexturesProperty; +use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\Handler\MockHandler; @@ -309,7 +311,7 @@ class ApiTest extends TestCase { } $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('You cannot request more than 100 names per request'); + $this->expectExceptionMessage('You cannot request more than 10 names per request'); $this->api->playernamesToUuids($names); } @@ -609,6 +611,130 @@ class ApiTest extends TestCase { $this->assertInstanceOf(ClientInterface::class, $child->getDefaultClient()); } + public function testIsSecurityQuestionsNeeded() { + $this->mockHandler->append(new Response(204)); + $this->expectException(NoContentException::class); + $this->api->isSecurityQuestionsNeeded('mocked access token'); + } + + public function testIsSecurityQuestionsNeededOperationException() { + $this->mockHandler->append($this->createResponse(200, [ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Current IP is not secured', + ])); + $this->expectException(OperationException::class); + $this->api->isSecurityQuestionsNeeded('mocked access token'); + } + + public function testQuestions() { + $this->mockHandler->append($this->createResponse(200, [ + [ + 'answer' => [ + 'id' => 123, + ], + 'question' => [ + 'id' => 1, + 'question' => 'What is your favorite pet\'s name?', + ], + ], + [ + 'answer' => [ + 'id' => 456, + ], + 'question' => [ + 'id' => 2, + 'question' => 'What is your favorite movie?', + ], + ], + [ + 'answer' => [ + 'id' => 789, + ], + 'question' => [ + 'id' => 3, + 'question' => 'What is your favorite author\'s last name?', + ], + ], + ])); + $result = $this->api->questions('mocked access token'); + /** @var \Psr\Http\Message\RequestInterface $request */ + $request = $this->history[0]['request']; + $this->assertSame('Bearer mocked access token', $request->getHeaderLine('Authorization')); + + foreach ($result as $question) { + $this->assertInstanceOf(QuestionResponse::class, $question); + } + + /** @var QuestionResponse $firstQuestion */ + $firstQuestion = $result[0]; + $this->assertSame(123, $firstQuestion->getAnswerId()); + $this->assertSame(1, $firstQuestion->getQuestionId()); + $this->assertSame('What is your favorite pet\'s name?', $firstQuestion->getQuestion()); + + /** @var QuestionResponse $secondQuestion */ + $secondQuestion = $result[1]; + $this->assertSame(456, $secondQuestion->getAnswerId()); + $this->assertSame(2, $secondQuestion->getQuestionId()); + $this->assertSame('What is your favorite movie?', $secondQuestion->getQuestion()); + + /** @var QuestionResponse $thirdQuestion */ + $thirdQuestion = $result[2]; + $this->assertSame(789, $thirdQuestion->getAnswerId()); + $this->assertSame(3, $thirdQuestion->getQuestionId()); + $this->assertSame('What is your favorite author\'s last name?', $thirdQuestion->getQuestion()); + } + + public function testAnswerOperationException() { + $this->mockHandler->append($this->createResponse(200, [ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'At least one answer was incorrect', + ])); + $result = $this->api->answer('mocked access token', [ + [ + 'id' => 123, + 'answer' => 'foo', + ], + [ + 'id' => 456, + 'answer' => 'bar', + ], + ]); + $this->assertFalse($result); + } + + public function testAnswer() { + $this->mockHandler->append(new Response(204)); + $this->expectException(NoContentException::class); + $this->api->isSecurityQuestionsNeeded('mocked access token'); + } + + public function testStatistics() { + $this->mockHandler->append($this->createResponse(200, [ + 'total' => 145, + 'last24h' => 13, + 'saleVelocityPerSeconds' => 1.32, + ])); + $result = $this->api->statistics([ + 'item_sold_minecraft', + 'prepaid_card_redeemed_minecraft', + 'item_sold_cobalt', + 'item_sold_scrolls', + ]); + /** @var \Psr\Http\Message\RequestInterface $request */ + $request = $this->history[0]['request']; + $params = json_decode($request->getBody()->getContents(), true); + $this->assertSame([ + 'item_sold_minecraft', + 'prepaid_card_redeemed_minecraft', + 'item_sold_cobalt', + 'item_sold_scrolls', + ], $params['metricKeys']); + + $this->assertSame(145, $result->getTotal()); + $this->assertSame(13, $result->getLast24H()); + $this->assertSame(1.32, $result->getSaleVelocityPerSeconds()); + } + private function createResponse(int $statusCode, array $response): ResponseInterface { return new Response($statusCode, ['content-type' => 'json'], json_encode($response)); }