diff --git a/src/Api.php b/src/Api.php index 55c3eda..85667c2 100644 --- a/src/Api.php +++ b/src/Api.php @@ -6,6 +6,8 @@ namespace Ely\Mojang; use DateTime; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; +use Ely\Mojang\Response\AnswerResponse; +use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; @@ -150,8 +152,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 +454,85 @@ 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 { + $uri = new Uri('https://api.mojang.com/user/security/location'); + $request = new Request('GET', $uri, ['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 { + $uri = new Uri('https://api.mojang.com/user/security/challenges'); + $request = new Request('GET', $uri, ['Authorization' => 'Bearer ' . $accessToken]); + $response = $this->getClient()->send($request); + $rawBody = $response->getBody()->getContents(); + if (empty($rawBody)) { + throw new Exception\NoContentException($request, $response); + } + + $result = []; + $body = $this->decode($rawBody); + foreach ($body as $question) { + $result[] = [ + 'answer' => new AnswerResponse($question['answer']['id']), + 'question' => new QuestionResponse($question['question']['id'], $question['question']['question']), + ]; + } + + return $result; + } + + /** + * @param string $accessToken + * @param array $answers + * @throws GuzzleException + * + * @url https://wiki.vg/Mojang_API#Send_back_the_answers + */ + public function answer(string $accessToken, array $answers): void { + $uri = new Uri('https://api.mojang.com/user/security/location'); + $request = new Request('POST', $uri, ['Authorization' => 'Bearer ' . $accessToken], json_encode($answers)); + $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 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, + ]); + $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 @@ +id = $id; + $this->answer = $answer; + } + + public function getId(): int { + return $this->id; + } + + public function getAnswer(): ?string { + return $this->answer; + } + +} diff --git a/src/Response/QuestionResponse.php b/src/Response/QuestionResponse.php new file mode 100644 index 0000000..997790d --- /dev/null +++ b/src/Response/QuestionResponse.php @@ -0,0 +1,31 @@ +id = $id; + $this->question = $question; + } + + public function getId(): int { + return $this->id; + } + + public function getQuestion(): string { + return $this->question; + } + +} 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..7d9a872 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -6,12 +6,15 @@ 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\AnswerResponse; 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 +312,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 +612,144 @@ 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->assertArrayHasKey('answer', $question); + $this->assertArrayHasKey('question', $question); + $this->assertInstanceOf(AnswerResponse::class, $question['answer']); + $this->assertInstanceOf(QuestionResponse::class, $question['question']); + } + + /** @var AnswerResponse $firstAnswer */ + $firstAnswer = $result[0]['answer']; + /** @var QuestionResponse $firstQuestion */ + $firstQuestion = $result[0]['question']; + $this->assertSame(123, $firstAnswer->getId()); + $this->assertNull($firstAnswer->getAnswer()); + $this->assertSame(1, $firstQuestion->getId()); + $this->assertSame('What is your favorite pet\'s name?', $firstQuestion->getQuestion()); + + /** @var AnswerResponse $secondAnswer */ + $secondAnswer = $result[1]['answer']; + /** @var QuestionResponse $secondQuestion */ + $secondQuestion = $result[1]['question']; + $this->assertSame(456, $secondAnswer->getId()); + $this->assertNull($secondAnswer->getAnswer()); + $this->assertSame(2, $secondQuestion->getId()); + $this->assertSame('What is your favorite movie?', $secondQuestion->getQuestion()); + + /** @var AnswerResponse $thirdAnswer */ + $thirdAnswer = $result[2]['answer']; + /** @var QuestionResponse $thirdQuestion */ + $thirdQuestion = $result[2]['question']; + $this->assertSame(789, $thirdAnswer->getId()); + $this->assertNull($thirdAnswer->getAnswer()); + $this->assertSame(3, $thirdQuestion->getId()); + $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', + ])); + $this->expectException(OperationException::class); + $this->api->answer('mocked access token', [ + [ + 'id' => 123, + 'answer' => 'foo', + ], + [ + 'id' => 456, + 'answer' => 'bar', + ], + ]); + } + + 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([ + 'metricKeys' => [ + '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)); }