├── .gitignore ├── src ├── Exception │ ├── FilterException.php │ ├── NoTrophiesException.php │ ├── NotFoundException.php │ ├── MissingPlatformException.php │ ├── MissingKeyPairIdException.php │ └── UnmappedGraphQLOperationException.php ├── Interfaces │ └── FactoryInterface.php ├── Enum │ ├── UgcType.php │ ├── CloudStatusType.php │ ├── SessionType.php │ ├── TranscodeStatusType.php │ ├── TrophyType.php │ ├── ConsoleType.php │ ├── DescriptionType.php │ ├── MessageType.php │ └── LanguageType.php ├── Model │ ├── Message │ │ ├── Sendable.php │ │ ├── AudioMessage.php │ │ ├── VideoMessage.php │ │ ├── TextMessage.php │ │ ├── ImageMessage.php │ │ ├── StickerMessage.php │ │ └── AbstractMessage.php │ ├── Trophy │ │ ├── TrophyTitle.php │ │ ├── AbstractTrophyTitle.php │ │ ├── TrophySummary.php │ │ ├── Trophy.php │ │ ├── TrophyGroup.php │ │ └── UserTrophyTitle.php │ ├── MessageThread.php │ ├── Store │ │ └── Concept.php │ ├── Media.php │ ├── GameTitle.php │ ├── Group.php │ └── User.php ├── Iterator │ ├── Filter │ │ ├── TitleIdFilter.php │ │ ├── User │ │ │ ├── VerifiedUserFilter.php │ │ │ ├── OnlineIdFilter.php │ │ │ └── CloseFriendFilter.php │ │ ├── TrophyGroup │ │ │ ├── NameFilter.php │ │ │ ├── DetailFilter.php │ │ │ └── TrophyTypeFilter.php │ │ ├── MessageTypeFilter.php │ │ ├── TrophyTitle │ │ │ ├── TrophyTitleHasGroupsFilter.php │ │ │ └── TrophyTitleNameFilter.php │ │ └── GroupMembersFilter.php │ ├── GameListIterator.php │ ├── GroupsIterator.php │ ├── CloudMediaGalleryIterator.php │ ├── TrophyTitlesIterator.php │ ├── TrophyIterator.php │ ├── FriendsListIterator.php │ ├── StoreSearchIterator.php │ ├── TrophyGroupsIterator.php │ ├── UsersSearchIterator.php │ ├── MessagesIterator.php │ └── AbstractApiIterator.php ├── Factory │ ├── StoreFactory.php │ ├── TrophyFactory.php │ ├── UsersFactory.php │ ├── MessagesFactory.php │ ├── GameListFactory.php │ ├── GroupMembersFactory.php │ ├── TrophyGroupsFactory.php │ ├── FriendsListFactory.php │ ├── CloudMediaGalleryFactory.php │ ├── GroupsFactory.php │ └── TrophyTitlesFactory.php ├── Http │ └── ResponseParser.php ├── OAuthToken.php ├── Traits │ └── OperandParser.php ├── Api.php ├── Model.php └── Client.php ├── tests ├── Traits │ ├── OperandParserTester.php │ └── OperandParserTest.php ├── ModelTest.php ├── OAuthTokenTest.php ├── ApiTest.php └── ClientTest.php ├── .gitattributes ├── .github └── stale.yml ├── phpunit.xml ├── README.md ├── composer.json ├── LICENSE └── intercept.js /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | /.idea 3 | test.php 4 | .vscode/launch.json 5 | composer.lock 6 | /.phpunit.result.cache -------------------------------------------------------------------------------- /src/Exception/FilterException.php: -------------------------------------------------------------------------------- 1 | operator = $operator; 14 | } 15 | 16 | public function parseIt($lhs, $rhs): bool 17 | { 18 | return $this->parse($lhs, $rhs); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Enum/MessageType.php: -------------------------------------------------------------------------------- 1 | current()->titleId() === $this->value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /src/Iterator/Filter/User/VerifiedUserFilter.php: -------------------------------------------------------------------------------- 1 | current()->isVerified() === true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Model/Message/AudioMessage.php: -------------------------------------------------------------------------------- 1 | current()->name(), $this->groupName) !== false; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Iterator/Filter/User/OnlineIdFilter.php: -------------------------------------------------------------------------------- 1 | current()->onlineId(), $this->onlineId) !== false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Iterator/Filter/TrophyGroup/DetailFilter.php: -------------------------------------------------------------------------------- 1 | current()->detail(), $this->detail) !== false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Iterator/Filter/MessageTypeFilter.php: -------------------------------------------------------------------------------- 1 | current(); 18 | $b = $this->type; 19 | return $a instanceof $b; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Iterator/Filter/TrophyTitle/TrophyTitleHasGroupsFilter.php: -------------------------------------------------------------------------------- 1 | current()->hasTrophyGroups() === $this->value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Iterator/Filter/TrophyTitle/TrophyTitleNameFilter.php: -------------------------------------------------------------------------------- 1 | current()->name(), $this->titleName) !== false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/ModelTest.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty($this->model->pluck('property')); 17 | } 18 | 19 | protected function setUp(): void 20 | { 21 | parent::setUp(); 22 | 23 | $this->model = $this->getMockForAbstractClass(Model::class, [$this->createMock(Client::class)]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /src/Factory/StoreFactory.php: -------------------------------------------------------------------------------- 1 | parse($this->current()->trophyCount($this->trophyType), $this->count); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | src 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSN-PHP Wrapper 2 | 3 | A PHP wrapper for the PlayStation API. 4 | 5 | [![GitHub stars](https://img.shields.io/github/stars/Tustin/psn-php.svg)](https://github.com/Tustin/psn-php/stargazers) 6 | [![GitHub license](https://img.shields.io/github/license/Tustin/psn-php.svg)](https://github.com/Tustin/psn-php/blob/master/LICENSE) 7 | 8 | ## Getting Started 9 | 10 | Pull in the project with composer: 11 | `composer require tustin/psn-php` 12 | 13 | ## Documentation 14 | 15 | Please visit [the psn-php documentation](https://tustin.dev/psn-php/) page to see how to use this library. 16 | 17 | ## Disclaimer 18 | 19 | This project was not intended to be used for spam, abuse, or anything of the sort. Any use of this project for those purposes is not endorsed. Please keep this in mind when creating applications using this API wrapper. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tustin/psn-php", 3 | "license": "MIT", 4 | "description": "PHP wrapper for the PlayStation API.", 5 | "homepage": "https://github.com/Tustin/psn-php", 6 | "keywords": ["playstation", "api", "php"], 7 | "autoload": { 8 | "psr-4": { 9 | "Tustin\\PlayStation\\": "src/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "Tests\\": "tests/" 15 | } 16 | }, 17 | "require": { 18 | "guzzlehttp/guzzle": "^7.5", 19 | "php": "^8.1", 20 | "tustin/haste": "^1.1", 21 | "nesbot/carbon": "^2.41 || ^3.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.5" 25 | }, 26 | "scripts": { 27 | "phpunit": "composer dump-autoload && vendor/bin/phpunit" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Iterator/Filter/User/CloseFriendFilter.php: -------------------------------------------------------------------------------- 1 | current()->getCache()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Model/Message/VideoMessage.php: -------------------------------------------------------------------------------- 1 | messageThread()->getHttpClient(), 18 | $this->pluck('messageDetail.videoMessageDetail.ugcId') 19 | ); 20 | } 21 | 22 | /** 23 | * Gets the message type. 24 | */ 25 | public function type(): MessageType 26 | { 27 | return MessageType::Video; 28 | } 29 | 30 | public function fetch(): object 31 | { 32 | throw new \BadMethodCallException(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/ResponseParser.php: -------------------------------------------------------------------------------- 1 | getBody()->getContents(); 20 | 21 | $data = json_decode($contents); 22 | 23 | return (json_last_error() === JSON_ERROR_NONE) ? $data : (empty($contents) ? $response : $contents); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Model/Message/TextMessage.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function build(): array 29 | { 30 | return [ 31 | 'messageType' => $this->type(), 32 | 'body' => $this->textMessage 33 | ]; 34 | } 35 | 36 | public function fetch(): object 37 | { 38 | throw new \BadMethodCallException(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Model/Message/ImageMessage.php: -------------------------------------------------------------------------------- 1 | messageThread()->getHttpClient(), $this->pluck('messageDetail.imageMessageDetail.ugcId')); 17 | } 18 | 19 | /** 20 | * Gets the resource id. 21 | */ 22 | public function resourceId(): string 23 | { 24 | return $this->pluck('messageDetail.imageMessageDetail.resourceId'); 25 | } 26 | 27 | /** 28 | * Gets the message type. 29 | */ 30 | public function type(): MessageType 31 | { 32 | return MessageType::Image; 33 | } 34 | 35 | public function fetch(): object 36 | { 37 | throw new \BadMethodCallException(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Factory/TrophyFactory.php: -------------------------------------------------------------------------------- 1 | group); 29 | 30 | return $iterator; 31 | } 32 | 33 | /** 34 | * Gets the first trophy title in the collection. 35 | * 36 | * @return Trophy 37 | */ 38 | public function first(): Trophy 39 | { 40 | return $this->getIterator()->current(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Model/Trophy/TrophyTitle.php: -------------------------------------------------------------------------------- 1 | setnpCommunicationId($npCommunicationId); 14 | $this->setServiceName($serviceName); 15 | } 16 | /** 17 | * Gets the NP communication ID (NPWR_) for this trophy title. 18 | * 19 | * @return string 20 | */ 21 | public function npCommunicationId(): string 22 | { 23 | return $this->npCommunicationId; 24 | } 25 | 26 | /** 27 | * Gets the trophy service name. 28 | * 29 | * @return string 30 | */ 31 | public function serviceName(): string 32 | { 33 | return $this->serviceName; 34 | } 35 | 36 | // @TODO: Implement 37 | public function fetch(): object 38 | { 39 | throw new \BadMethodCallException(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Enum/LanguageType.php: -------------------------------------------------------------------------------- 1 | current(); 18 | 19 | $matchingMembersCount = count(array_filter($this->onlineIds, function ($onlineId) use ($thread) { 20 | return $thread->members()->contains($onlineId); 21 | })); 22 | 23 | // Redundant, but it helps prevent needing to count each thread's members when theres no reason to. 24 | if ($matchingMembersCount === 0) { 25 | return false; 26 | } 27 | 28 | return $this->includesOnly ? 29 | $matchingMembersCount == $thread->members()->count() - 1 : 30 | $matchingMembersCount > 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tustin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/OAuthTokenTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(Carbon::now()->addSeconds(60)->format('Y-m-d H:i:s'), $oAuthToken->getExpiration()->format('Y-m-d H:i:s')); 18 | $oAuthToken = new OAuthToken('some-token', 333); 19 | $this->assertEquals(Carbon::now()->addSeconds(333)->format('Y-m-d H:i:s'), $oAuthToken->getExpiration()->format('Y-m-d H:i:s')); 20 | } 21 | 22 | public function testItShouldThrow(): void 23 | { 24 | $this->expectException(\InvalidArgumentException::class); 25 | $this->expectExceptionMessage('expiresIn has to be an integer > 0'); 26 | new OAuthToken('some-token', -200); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/OAuthToken.php: -------------------------------------------------------------------------------- 1 | = $expiresIn) { 18 | throw new \InvalidArgumentException('expiresIn has to be an integer > 0'); 19 | } 20 | $this->token = $token; 21 | $this->seconds = $expiresIn; 22 | $this->expiration = Carbon::now()->addSeconds($expiresIn); 23 | } 24 | 25 | /** 26 | * Gets the OAuth token. 27 | * 28 | * @return string 29 | */ 30 | public function getToken(): string 31 | { 32 | return $this->token; 33 | } 34 | 35 | /** 36 | * Gets the OAuth token's expiration date and time. 37 | */ 38 | public function getExpiration(): \DateTime 39 | { 40 | return $this->expiration; 41 | } 42 | 43 | /** 44 | * Gets the OAuth token's expiration in seconds. 45 | */ 46 | public function getExpirationSeconds(): int 47 | { 48 | return $this->seconds; 49 | } 50 | } -------------------------------------------------------------------------------- /src/Iterator/GameListIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 13 | 14 | $this->limit = 100; 15 | 16 | $this->access(0); 17 | } 18 | 19 | /** 20 | * Accesses a new page of results. 21 | */ 22 | public function access(mixed $cursor): void 23 | { 24 | $body = [ 25 | 'limit' => $this->limit, 26 | 'offset' => $cursor, 27 | ]; 28 | 29 | $results = $this->get('gamelist/v2/users/' . $this->gameListFactory->getUser()->accountId() . '/titles', $body); 30 | 31 | $this->update($results->totalItemCount, $results->titles); 32 | } 33 | 34 | /** 35 | * Gets the current game title in the iterator. 36 | */ 37 | public function current(): GameTitle 38 | { 39 | return GameTitle::fromObject( 40 | $this->gameListFactory, 41 | $this->getFromOffset($this->currentOffset) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factory/UsersFactory.php: -------------------------------------------------------------------------------- 1 | getHttpClient(), $accountId); 33 | } 34 | 35 | /** 36 | * Get the logged in user's profile. 37 | * 38 | * @return User 39 | */ 40 | public function me(): User 41 | { 42 | // Resolve account id 43 | $response = $this->get('https://dms.api.playstation.com/api/v1/devices/accounts/me'); 44 | return new User($this->getHttpClient(), $response->accountId); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Model/Message/StickerMessage.php: -------------------------------------------------------------------------------- 1 | pluck('messageDetail.stickerMessageDetail.imageUrl'); 16 | } 17 | 18 | /** 19 | * Gets the sticker manifest url. 20 | */ 21 | public function manifestUrl(): string 22 | { 23 | return $this->pluck('messageDetail.stickerMessageDetail.manifestFileUrl'); 24 | } 25 | 26 | /** 27 | * Gets the sticker package id. 28 | */ 29 | public function packageId(): string 30 | { 31 | return $this->pluck('messageDetail.stickerMessageDetail.packageId'); 32 | } 33 | 34 | /** 35 | * Gets the sticker number. 36 | */ 37 | public function number(): string 38 | { 39 | return $this->pluck('messageDetail.stickerMessageDetail.number'); 40 | } 41 | 42 | /** 43 | * Gets the message type 44 | */ 45 | public function type(): MessageType 46 | { 47 | return MessageType::Sticker; 48 | } 49 | 50 | public function fetch(): object 51 | { 52 | throw new \BadMethodCallException(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Iterator/GroupsIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 13 | 14 | $this->limit = 20; 15 | 16 | $this->access(0); 17 | } 18 | 19 | /** 20 | * Accesses a new page of results. 21 | */ 22 | public function access(mixed $cursor): void 23 | { 24 | $results = $this->get('gamingLoungeGroups/v1/members/me/groups', [ 25 | 'favoriteFilter' => $this->groupsFactory->favorited ? 'favorite' : 'notFavorite', 26 | 'limit' => $this->limit, 27 | 'offset' => $cursor, 28 | 'includeFields' => 'groupName,groupIcon,members,mainThread,joinedTimestamp,modifiedTimestamp,totalGroupCount,isFavorite,existsNewArrival,partySessions' 29 | ]); 30 | 31 | $this->update($results->totalGroupCount, $results->groups); 32 | } 33 | 34 | /** 35 | * Gets the current group in the iterator. 36 | */ 37 | public function current(): Group 38 | { 39 | return Group::fromObject( 40 | $this->groupsFactory, 41 | $this->getFromOffset($this->currentOffset) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Iterator/CloudMediaGalleryIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 13 | 14 | $this->limit = 20; 15 | 16 | $this->access(0); 17 | } 18 | 19 | /** 20 | * Accesses a new page of results. 21 | */ 22 | public function access($cursor): void 23 | { 24 | $body = [ 25 | 'includeTokenizedUrls' => 'true', // Doesn't change anything 26 | 'limit' => $this->limit, 27 | // @TODO: Where does $cursor go?? Need more media to test this. 28 | ]; 29 | 30 | $results = $this->get('gameMediaService/v2/c2s/category/cloudMediaGallery/ugcType/all', $body); 31 | 32 | $this->update($this->limit, $results->ugcDocument, $results->nextCursorMark); 33 | } 34 | 35 | /** 36 | * Gets the current media in the iterator. 37 | */ 38 | public function current(): Media 39 | { 40 | return Media::fromObject( 41 | $this->cloudMediaGalleryFactory->getHttpClient(), 42 | $this->getFromOffset($this->currentOffset) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /intercept.js: -------------------------------------------------------------------------------- 1 | // https://my.account.sony.com/central/signin/?duid=00000007000901002ffe132f3275f2c6c581276b3b3ea1c581a41f31a9fa4bb9194d7a8cf2604a37&response_type=code&client_id=e4a62faf-4b87-4fea-8565-caaabb3ac918&scope=web%3Acore&access_type=offline&state=335d242f0d66f8e0fc7d8f98ccec05764ed91022a5b5f110c38f68f7849657cf&service_entity=urn%3Aservice-entity%3Apsn&ui=pr&redirect_uri=https%3A%2F%2Fweb.np.playstation.com%2Fapi%2Fsession%2Fv1%2Fsession%3Fredirect_uri%3Dhttps%253A%252F%252Fstore.playstation.com%252Fen-us%252Flatest%26x-psn-app-ver%3D%2540sie-ppr-web-session%252Fsession%252Fv4.3.2&auth_ver=v3&error=login_required&error_code=4165&error_description=User+is+not+authenticated&no_captcha=true&cid=05eaf704-f788-47c1-8e6e-ecbaa1dde877#/signin/ca?entry=ca 2 | (function(open) { 3 | XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { 4 | 5 | this.addEventListener("readystatechange", function() { 6 | if (this.readyState == XMLHttpRequest.DONE) { 7 | let response = JSON.parse(this.responseText); 8 | 9 | if (response && "npsso" in response) { 10 | console.log('found npsso', response.npsso); 11 | } 12 | } 13 | }, false); 14 | 15 | open.call(this, method, url, async, user, pass); 16 | }; 17 | 18 | window.onbeforeunload = function(){ 19 | return 'Are you sure you want to leave?'; 20 | }; 21 | 22 | })(XMLHttpRequest.prototype.open); -------------------------------------------------------------------------------- /src/Traits/OperandParser.php: -------------------------------------------------------------------------------- 1 | operator) 20 | { 21 | throw new RuntimeException('No such property [operator] exists on class [' . get_class($this) . '], which uses OperandParser.'); 22 | } 23 | 24 | if (!is_string($this->operator)) 25 | { 26 | throw new RuntimeException("Operator value [$this->operator] is not a string."); 27 | } 28 | 29 | switch ($this->operator) 30 | { 31 | case '=': 32 | case '==': 33 | case '===': 34 | return $lhs === $rhs; 35 | case '>': 36 | return $lhs > $rhs; 37 | case '>=': 38 | return $lhs >= $rhs; 39 | case '<': 40 | return $lhs < $rhs; 41 | case '<=': 42 | return $lhs <= $rhs; 43 | case '!=': 44 | case '!=': 45 | case '<>': 46 | return $lhs !== $rhs; 47 | default: 48 | throw new RuntimeException("Operator value [$this->operator] is not supported."); 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /src/Iterator/TrophyTitlesIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 15 | 16 | // $this->platforms = implode(',', $trophyTitlesFactory->getPlatforms()); 17 | 18 | $this->limit = 100; 19 | 20 | $this->access(0); 21 | } 22 | 23 | /** 24 | * Accesses a new page of results. 25 | */ 26 | public function access(mixed $cursor): void 27 | { 28 | $body = [ 29 | 'limit' => $this->limit, 30 | 'offset' => $cursor, 31 | ]; 32 | 33 | $results = $this->get('trophy/v1/users/' . $this->trophyTitlesFactory->getUser()->accountId() . '/trophyTitles', $body); 34 | 35 | $this->update($results->totalItemCount, $results->trophyTitles); 36 | } 37 | 38 | /** 39 | * Gets the current user trophy title in the iterator. 40 | */ 41 | public function current(): UserTrophyTitle 42 | { 43 | $title = new UserTrophyTitle($this->trophyTitlesFactory->getHttpClient()); 44 | $title->setFactory($this->trophyTitlesFactory); 45 | $title->setCache($this->getFromOffset($this->currentOffset)); 46 | 47 | return $title; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Factory/MessagesFactory.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 21 | } 22 | 23 | /** 24 | * Gets messages only of a certain type. 25 | */ 26 | public function of(string $class): MessagesFactory 27 | { 28 | $this->typeFilter = $class; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Gets the iterator and applies any filters. 35 | */ 36 | public function getIterator(): Iterator 37 | { 38 | $iterator = new MessagesIterator($this->thread); 39 | 40 | if ($this->typeFilter && class_exists($this->typeFilter) !== false) { 41 | $iterator = new MessageTypeFilter($iterator, $this->typeFilter); 42 | } 43 | 44 | return $iterator; 45 | } 46 | 47 | /** 48 | * Gets the first message in the message thread. 49 | */ 50 | public function first(): AbstractMessage 51 | { 52 | return $this->getIterator()->current(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Model/Trophy/AbstractTrophyTitle.php: -------------------------------------------------------------------------------- 1 | npCommunicationId = $npCommunicationId; 30 | } 31 | 32 | /** 33 | * Sets the service name for this trophy title. 34 | */ 35 | protected function setServiceName(string $serviceName) 36 | { 37 | $this->serviceName = $serviceName; 38 | } 39 | 40 | /** 41 | * Gets all the trophy groups for the trophy title. 42 | */ 43 | public function trophyGroups(): TrophyGroupsFactory 44 | { 45 | return new TrophyGroupsFactory($this); 46 | } 47 | 48 | /** 49 | * Gets the NP communication ID (NPWR_) for this trophy title. 50 | */ 51 | public abstract function npCommunicationId(): string; 52 | 53 | /** 54 | * Gets the service name for this trophy title. 55 | * 56 | * PS5 has a different service name than PS4 so this needs to be set correctly to avoid errors. 57 | */ 58 | public abstract function serviceName(): string; 59 | } 60 | -------------------------------------------------------------------------------- /src/Iterator/TrophyIterator.php: -------------------------------------------------------------------------------- 1 | title()->getHttpClient()); 14 | 15 | $this->access(0); 16 | } 17 | 18 | /** 19 | * Accesses a new page of results. 20 | */ 21 | public function access(mixed $cursor): void 22 | { 23 | if ($this->trophyGroup->title() instanceof UserTrophyTitle) { 24 | $results = $this->get( 25 | 'trophy/v1/users/' . $this->trophyGroup->title()->getFactory()->getUser()->accountId() . '/npCommunicationIds/' . $this->trophyGroup->title()->npCommunicationId() . '/trophyGroups/' . $this->trophyGroup->id() . '/trophies', 26 | [ 27 | 'npServiceName' => $this->trophyGroup->title()->serviceName() 28 | ] 29 | ); 30 | } else { 31 | $results = $this->get( 32 | 'trophy/v1/npCommunicationIds/' . $this->trophyGroup->title()->npCommunicationId() . '/trophyGroups/' . $this->trophyGroup->id() . '/trophies', 33 | [ 34 | 'npServiceName' => $this->trophyGroup->title()->serviceName(), 35 | 'offset' => $cursor 36 | ] 37 | ); 38 | } 39 | 40 | $this->update($results->totalItemCount, $results->trophies); 41 | } 42 | 43 | /** 44 | * Gets the current trophy in the iterator. 45 | */ 46 | public function current(): Trophy 47 | { 48 | return Trophy::fromObject( 49 | $this->trophyGroup, 50 | $this->getFromOffset($this->currentOffset), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Model/Trophy/TrophySummary.php: -------------------------------------------------------------------------------- 1 | user = $user; 13 | 14 | parent::__construct($user->getHttpClient()); 15 | } 16 | 17 | /** 18 | * Gets the trophy level progress for the current level. 19 | */ 20 | public function progress(): int 21 | { 22 | return $this->pluck('progress'); 23 | } 24 | 25 | /** 26 | * Gets the trophy level tier. 27 | * 28 | * @TODO: Maybe map out each tier and use an enum here? 29 | */ 30 | public function tier(): int 31 | { 32 | return $this->pluck('tier'); 33 | } 34 | 35 | /** 36 | * Gets the trophy level. 37 | */ 38 | public function level(): int 39 | { 40 | return $this->pluck('trophyLevel'); 41 | } 42 | 43 | /** 44 | * Gets the amount of bronze trophies. 45 | */ 46 | public function bronze(): int 47 | { 48 | return $this->pluck('earnedTrophies.bronze'); 49 | } 50 | 51 | /** 52 | * Gets the amount of silver trophies. 53 | */ 54 | public function silver(): int 55 | { 56 | return $this->pluck('earnedTrophies.silver'); 57 | } 58 | 59 | /** 60 | * Gets the amount of gold trophies. 61 | */ 62 | public function gold(): int 63 | { 64 | return $this->pluck('earnedTrophies.gold'); 65 | } 66 | 67 | 68 | /** 69 | * Gets the amount of platinum trophies. 70 | */ 71 | public function platinum(): int 72 | { 73 | return $this->pluck('earnedTrophies.platinum'); 74 | } 75 | 76 | /** 77 | * Fetches the trophy summary from the API. 78 | */ 79 | public function fetch(): object 80 | { 81 | return $this->get('trophy/v1/users/' . $this->user->accountId() . '/trophySummary'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Factory/GameListFactory.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 18 | } 19 | 20 | /** 21 | * Filters game list to only get games for the specific user. 22 | * 23 | * @param User $user 24 | * @return GameListFactory 25 | */ 26 | public function forUser(User $user): GameListFactory 27 | { 28 | $this->user = $user; 29 | 30 | return $this; 31 | } 32 | /** 33 | * Gets the iterator and applies any filters. 34 | * 35 | * @return Iterator 36 | */ 37 | public function getIterator(): Iterator 38 | { 39 | $iterator = new GameListIterator($this); 40 | 41 | return $iterator; 42 | } 43 | 44 | /** 45 | * Gets the current user (if specified) to get game list for. 46 | * 47 | * @return User|null 48 | */ 49 | public function getUser(): ?User 50 | { 51 | return $this->user; 52 | } 53 | 54 | /** 55 | * Checks to see if this factory should be looking at a specific user's game list. 56 | * 57 | * @return boolean 58 | */ 59 | public function hasUser(): bool 60 | { 61 | return $this->user !== null; 62 | } 63 | 64 | /** 65 | * Gets the first game entry in the collection. 66 | * 67 | * @return GameTitle 68 | */ 69 | public function first(): GameTitle 70 | { 71 | try 72 | { 73 | return $this->getIterator()->current(); 74 | } 75 | catch (InvalidArgumentException $e) 76 | { 77 | throw $e; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Iterator/FriendsListIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 18 | $this->limit = 100; 19 | $this->access(0); 20 | } 21 | 22 | /** 23 | * Accesses a new page of results. 24 | */ 25 | public function access(mixed $cursor): void 26 | { 27 | $results = $this->get('userProfile/v1/internal/users/' . $this->userAccountId . '/friends', [ 28 | 'limit' => $this->limit, 29 | 'offset' => $cursor, 30 | 'order' => 'availability+realName+onlineId' 31 | ]); 32 | 33 | // Batch-fetch these friends so that we can run filters over the properties without needing to fetch each individual profile. 34 | $friendDetails = $this->get('userProfile/v1/internal/users/profiles', [ 35 | 'accountIds' => implode(',', $results->friends) 36 | ]); 37 | 38 | $this->cachedAccounts = array_merge($this->cachedAccounts, $friendDetails->profiles); 39 | 40 | $this->update($results->totalItemCount, $results->friends); 41 | } 42 | 43 | /** 44 | * Gets the current user in the iterator. 45 | */ 46 | public function current(): User 47 | { 48 | // Because there's no accountId prop in the batch profile responses, we need to manually add it. 49 | // Cmon Sony... 50 | $this->cachedAccounts[$this->currentOffset]->accountId = $this->cache[$this->currentOffset]; 51 | 52 | return User::fromObject( 53 | $this->getHttpClient(), 54 | $this->cachedAccounts[$this->currentOffset] 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Iterator/StoreSearchIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 21 | $this->access(''); 22 | } 23 | 24 | /** 25 | * Accesses a new page of results. 26 | */ 27 | public function access(mixed $cursor): void 28 | { 29 | $results = $this->postJson('search/v1/universalSearch', [ 30 | 'age' => '69', 31 | 'countryCode' => $this->countryCode, 32 | 'domainRequests' => [ 33 | [ 34 | 'domain' => 'ConceptGameMobileApp', 35 | 'pagination' => [ 36 | 'cursor' => $cursor, 37 | 'pageSize' => '20' // @TODO: Test if this can be altered. 38 | ] 39 | ] 40 | ], 41 | 'languageCode' => $this->languageCode, 42 | 'searchTerm' => $this->query 43 | ]); 44 | 45 | $this->update($results->domainResponses[0]->totalResultCount, $results->domainResponses[0]->results, $results->domainResponses[0]->next); 46 | } 47 | 48 | public function next(): void 49 | { 50 | $this->currentOffset++; 51 | if (($this->currentOffset % $this->limit) == 0) { 52 | $this->access($this->maxEventIndexCursor); 53 | } 54 | } 55 | 56 | /** 57 | * Gets the current concept in the iterator. 58 | */ 59 | public function current(): Concept 60 | { 61 | $concept = $this->getFromOffset($this->currentOffset)->conceptProductMetadata; 62 | 63 | return Concept::fromObject($this->getHttpClient(), $concept); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Iterator/TrophyGroupsIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 14 | 15 | $this->access(0); 16 | } 17 | 18 | /** 19 | * Accesses a new page of results. 20 | */ 21 | public function access(mixed $cursor): void 22 | { 23 | if ($this->title instanceof UserTrophyTitle) { 24 | $results = $this->get( 25 | 'trophy/v1/users/' . $this->title->getFactory()->getUser()->accountId() . '/npCommunicationIds/' . $this->title->npCommunicationId() . '/trophyGroups', 26 | [ 27 | 'npServiceName' => $this->title->serviceName() 28 | ] 29 | ); 30 | } else { 31 | $results = $this->get( 32 | 'trophy/v1/npCommunicationIds/' . $this->title->npCommunicationId() . '/trophyGroups', 33 | [ 34 | 'npServiceName' => $this->title->serviceName() 35 | ] 36 | ); 37 | } 38 | 39 | $this->update(count($results->trophyGroups), $results->trophyGroups); 40 | } 41 | 42 | /** 43 | * Gets the current trophy group in the iterator. 44 | */ 45 | public function current(): TrophyGroup 46 | { 47 | if ($this->title instanceof UserTrophyTitle) { 48 | return new TrophyGroup($this->title, $this->getFromOffset($this->currentOffset)->trophyGroupId); 49 | } else { 50 | return new TrophyGroup( 51 | $this->title, 52 | $this->getFromOffset($this->currentOffset)->trophyGroupId, 53 | $this->getFromOffset($this->currentOffset)->trophyGroupName, 54 | $this->getFromOffset($this->currentOffset)->trophyGroupIconUrl, 55 | $this->getFromOffset($this->currentOffset)->trophyGroupDetail ?? '' 56 | ); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Iterator/UsersSearchIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 17 | $this->limit = 50; 18 | $this->access(''); 19 | } 20 | 21 | /** 22 | * Accesses a new page of results. 23 | */ 24 | public function access(mixed $cursor): void 25 | { 26 | // @TODO: Since the search function seems to be streamlined now, we could probably throw this into the abstract api iterator?? 27 | $results = $this->postJson('search/v1/universalSearch', [ 28 | 'age' => '69', 29 | 'countryCode' => $this->countryCode, 30 | 'domainRequests' => [ 31 | [ 32 | 'domain' => 'SocialAllAccounts', 33 | 'pagination' => [ 34 | 'cursor' => $cursor, 35 | 'pageSize' => '50' // 50 is max. 36 | ] 37 | ] 38 | ], 39 | 'languageCode' => $this->languageCode, 40 | 'searchTerm' => $this->query 41 | ]); 42 | 43 | $domainResponse = $results->domainResponses[0]; 44 | 45 | $this->update($domainResponse->totalResultCount, $domainResponse->results, $domainResponse->next ?? ""); 46 | } 47 | 48 | /** 49 | * Gets the current user in the iterator. 50 | */ 51 | public function current(): User 52 | { 53 | $socialMetadata = $this->getFromOffset($this->currentOffset)->socialMetadata; 54 | //$token = $this->getFromOffset($this->currentOffset)->id; // Do we need this?? 55 | 56 | return User::fromObject( 57 | $this->usersFactory->getHttpClient(), 58 | $socialMetadata 59 | )->setCountry($socialMetadata->country); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Model/MessageThread.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 16 | } 17 | 18 | /** 19 | * Creates a new message thread from existing data. 20 | */ 21 | public static function fromObject(Group $group, object $data): self 22 | { 23 | $instance = new static($group, $data->threadId); 24 | $instance->setCache($data); 25 | 26 | return $instance; 27 | } 28 | 29 | /** 30 | * Sends a message to the message thread. 31 | */ 32 | public function sendMessage(Sendable $message): AbstractMessage 33 | { 34 | $this->postJson( 35 | 'gamingLoungeGroups/v1/groups/' . $this->group()->id() . '/threads/' . $this->id() . '/messages', 36 | $message->build() 37 | ); 38 | 39 | return $this->messages()->first(); 40 | } 41 | 42 | /** 43 | * Gets all messages in this message thread. 44 | * 45 | * @return MessagesFactory 46 | */ 47 | public function messages(): MessagesFactory 48 | { 49 | return new MessagesFactory($this); 50 | } 51 | 52 | /** 53 | * The thread id. 54 | * 55 | * @return string 56 | */ 57 | public function id(): string 58 | { 59 | return $this->threadId ??= $this->pluck('threadId'); 60 | } 61 | 62 | /** 63 | * The message group for this thread. 64 | * 65 | * @return Group 66 | */ 67 | public function group(): Group 68 | { 69 | return $this->group; 70 | } 71 | 72 | /** 73 | * The message count for this thread. 74 | * 75 | * @return integer 76 | */ 77 | public function messageCount(): int 78 | { 79 | return $this->pluck('messageCount') ?? 0; 80 | } 81 | 82 | // @TODO: Implement this. 83 | public function fetch(): object 84 | { 85 | throw new \BadMethodCallException(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Iterator/MessagesIterator.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 20 | $this->access(null); 21 | } 22 | 23 | /** 24 | * Accesses the API to get the next set of messages. 25 | */ 26 | public function access(mixed $cursor): void 27 | { 28 | $params = []; 29 | 30 | if ($cursor != null) { 31 | if (!is_string($cursor)) { 32 | throw new InvalidArgumentException("$cursor must be a string."); 33 | } 34 | 35 | $params['before'] = $cursor; 36 | } 37 | 38 | $results = $this->get('gamingLoungeGroups/v1/members/me/groups/' . $this->thread->group()->id() . '/threads/' . $this->thread->id() . '/messages', $params); 39 | 40 | $this->totalCount += $results->messageCount; 41 | // if ($results->reachedEndOfPage && $results->messageCount == 0) { 42 | // return; 43 | // } 44 | 45 | // $this->force(!$results->reachedEndOfPage); 46 | $this->update($this->totalCount, $results->messages, $results->previous); 47 | } 48 | 49 | /** 50 | * Moves the iterator to the next message. 51 | */ 52 | public function next(): void 53 | { 54 | $this->currentOffset++; 55 | 56 | // Since totalResults for the messages API just returns the amount of messages sent in the response, we have to do it like this. 57 | if ($this->currentOffset == $this->totalResults) { 58 | $this->access($this->customCursor); 59 | } 60 | } 61 | 62 | /** 63 | * Gets the current message in the iterator. 64 | * 65 | * Will automatically convert the message to a specific type of message. 66 | */ 67 | public function current(): AbstractMessage 68 | { 69 | return AbstractMessage::create( 70 | $this->thread, 71 | $this->getFromOffset($this->currentOffset) 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Factory/GroupMembersFactory.php: -------------------------------------------------------------------------------- 1 | name = $name; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Returns whether or not a member with the onlineId exists in this thread. 37 | * 38 | * @param string $onlineId 39 | * @return boolean 40 | */ 41 | public function contains(string $onlineId): bool 42 | { 43 | foreach ($this as $member) 44 | { 45 | if (strcasecmp($member->onlineId(), $onlineId) === 0) 46 | { 47 | return true; 48 | } 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * Returns whether or not this thread contains only the user supplied and the client. 56 | * 57 | * @param string $onlineId 58 | * @return boolean 59 | */ 60 | public function containsOnly(string $onlineId): bool 61 | { 62 | return $this->contains($onlineId) && $this->count() === 2; 63 | } 64 | 65 | /** 66 | * Gets the iterator and applies any filters. 67 | * 68 | * @return Iterator 69 | */ 70 | public function getIterator(): Iterator 71 | { 72 | $iterator = yield from array_map( 73 | fn($member) => new User($this->group->getHttpClient(), $member['accountId']), 74 | $this->group->membersArray() 75 | ); 76 | 77 | if ($this->name) 78 | { 79 | $iterator = new CallbackFilterIterator( 80 | $iterator, 81 | fn($it) => stripos($it->onlineId(), $this->name) !== false 82 | ); 83 | } 84 | 85 | return $iterator; 86 | } 87 | 88 | public function count(): int 89 | { 90 | return \count($this->messageThread->membersArray()); 91 | } 92 | } -------------------------------------------------------------------------------- /src/Factory/TrophyGroupsFactory.php: -------------------------------------------------------------------------------- 1 | withName = $name; 30 | 31 | return $this; 32 | } 33 | 34 | public function withDetail(string $detail) 35 | { 36 | $this->withDetail = $detail; 37 | 38 | return $this; 39 | } 40 | 41 | public function withTrophyCount(TrophyType $trophy, string $operand, int $count) 42 | { 43 | $this->certainTrophyTypeFilter[] = [$trophy, $operand, $count]; 44 | 45 | return $this; 46 | } 47 | 48 | public function withTotalTrophyCount(string $operand, int $count) 49 | { 50 | // 51 | } 52 | 53 | /** 54 | * Gets the iterator and applies any filters. 55 | * 56 | * @return Iterator 57 | */ 58 | public function getIterator(): Iterator 59 | { 60 | $iterator = new TrophyGroupsIterator($this->title); 61 | 62 | if ($this->withName) 63 | { 64 | $iterator = new NameFilter($iterator, $this->withName); 65 | } 66 | 67 | if ($this->withDetail) 68 | { 69 | $iterator = new DetailFilter($iterator, $this->withDetail); 70 | } 71 | 72 | if ($this->certainTrophyTypeFilter) 73 | { 74 | foreach ($this->certainTrophyTypeFilter as $filter) 75 | { 76 | $iterator = new TrophyTypeFilter($iterator, ...$filter); 77 | } 78 | } 79 | 80 | return $iterator; 81 | } 82 | 83 | /** 84 | * Gets the first trophy title in the collection. 85 | * 86 | * @return TrophyGroup 87 | */ 88 | public function first(): TrophyGroup 89 | { 90 | return $this->getIterator()->current(); 91 | } 92 | } -------------------------------------------------------------------------------- /src/Api.php: -------------------------------------------------------------------------------- 1 | httpClient = $client; 14 | } 15 | 16 | public function graphql(string $op, array $variables): object 17 | { 18 | // @Temp: This will hopefully be removed at some point for dynamic operation hashing. 19 | $hashMap = [ 20 | 'metGetConceptByProductIdQuery' => '0a4c9f3693b3604df1c8341fdc3e481f42eeecf961a996baaa65e65a657a6433', 21 | 'metGetConceptById' => 'cc90404ac049d935afbd9968aef523da2b6723abfb9d586e5f77ebf7c5289006', 22 | 'metGetProductById' => 'a128042177bd93dd831164103d53b73ef790d56f51dae647064cb8f9d9fc9d1a', 23 | 'metGetAddOnsByTitleId' => 'e98d01ff5c1854409a405a5f79b5a9bcd36a5c0679fb33f4e18113c157d4d916', 24 | 'metGetCategoryGrid'=> 'b67a9e4414b80d8d762bf12a588c6125467ae0bb3bbe3cee3f7696c6984f8ef6', 25 | 'metGetCategoryGrids'=> 'cc0b6513521c59a321bf62334fa23a92f22cd2ce1abe9f014fadac6379e414a8', 26 | // 'metGetCategoryStrand'=> '', 27 | 'metGetCategoryStrands'=> '55ab5f168bec56f8362b5519f59faaf786d4e1cfeabb8bc969d6a65545e14f4d', 28 | 'metGetDefaultView'=> 'bec1b8a3b0bae8c08e3ce2c7fe2f38a69343434ccfbcdd82cc1f2e44f86b7c40', 29 | // 'metGetFilterAndSortItemsCount'=> '', 30 | 'metGetPricingDataByConceptId'=> 'abcb311ea830e679fe2b697a27f755764535d825b24510ab1239a4ca3092bd09', 31 | 'metGetStoreWishlist'=> '571149e8aa4d76af7dd33b92e1d6f8f828ebc5fa8f0f6bf51a8324a0e6d71324', 32 | 'metGetViews'=> '6fd98ff7fecb603006fb5d92db176d5028435be163c8d1ee9f7c598ab4677dd1', 33 | 'metGetWebCheckoutCart'=> '2d4165c4de76877a32f3d08c91ce2af0e01d69300131fed0a8022868235e85b1', 34 | // 'metGetWishlistedItemIds'=> '', 35 | 'metGetExperience' => '054e61ee68bbeadc21435caebcc4f2bba0919a99b06629d141b0b82dc55f10c4' 36 | ]; 37 | 38 | if (!array_key_exists($op, $hashMap)) { 39 | throw new UnmappedGraphQLOperationException('The GraphQL operation ' . $op . ' is not mapped.'); 40 | } 41 | 42 | return $this->get('graphql/v1/op', [ 43 | 'operationName' => $op, 44 | 'variables' => json_encode($variables), 45 | 'extensions' => json_encode([ 46 | 'persistedQuery' => [ 47 | 'version' => 1, 48 | 'sha256Hash' => $hashMap[$op] 49 | ] 50 | ]) 51 | ])->data; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/Factory/FriendsListFactory.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 23 | } 24 | 25 | /** 26 | * Applies the filter for only querying close friends. 27 | * 28 | * @return FriendsListFactory 29 | */ 30 | public function closeFriends(): FriendsListFactory 31 | { 32 | $this->useCloseFriends = true; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Applies a filter for only querying users containing this online id. 39 | * 40 | * @param string $onlineId 41 | * @return FriendsListFactory 42 | */ 43 | public function onlineIdContains(string $onlineId): FriendsListFactory 44 | { 45 | $this->onlineId = $onlineId; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Applies a filter for only querying verified friends. 52 | * 53 | * @return FriendsListFactory 54 | */ 55 | public function verified(): FriendsListFactory 56 | { 57 | $this->verified = true; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Gets the iterator and applies any filters. 64 | * 65 | * @return Iterator 66 | */ 67 | public function getIterator(): Iterator 68 | { 69 | $iterator = new FriendsListIterator($this, $this->user->accountId()); 70 | 71 | if ($this->useCloseFriends) 72 | { 73 | $iterator = new CloseFriendFilter($iterator); 74 | } 75 | 76 | if ($this->verified) 77 | { 78 | $iterator = new VerifiedUserFilter($iterator); 79 | } 80 | 81 | if ($this->onlineId) 82 | { 83 | $iterator = new OnlineIdFilter($iterator, $this->onlineId); 84 | } 85 | 86 | return $iterator; 87 | } 88 | 89 | /** 90 | * Gets the first friend. 91 | * 92 | * @return User 93 | */ 94 | public function first(): User 95 | { 96 | return $this->getIterator()->current(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/Model/Message/AbstractMessage.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 21 | $instance->setCache($messageData); 22 | 23 | $instance->thread = $thread; 24 | 25 | return $instance; 26 | } 27 | 28 | /** 29 | * Gets the message type. 30 | */ 31 | public abstract function type(): MessageType; 32 | 33 | /** 34 | * Gets the message id. 35 | */ 36 | public function id(): string 37 | { 38 | return $this->pluck('messageUid'); 39 | } 40 | 41 | /** 42 | * Gets the body of the message. 43 | */ 44 | public function body(): string 45 | { 46 | return $this->pluck('body'); 47 | } 48 | 49 | /** 50 | * Gets the date and time when the message was posted. 51 | */ 52 | public function date(): \DateTime 53 | { 54 | return Carbon::parse($this->pluck('createdTimestamp'))->setTimezone('UTC'); 55 | } 56 | 57 | /** 58 | * Returns the message thread that this message is in. 59 | */ 60 | public function messageThread(): MessageThread 61 | { 62 | return $this->thread; 63 | } 64 | 65 | /** 66 | * Gets the message sender. 67 | */ 68 | public function sender(): User 69 | { 70 | return new User( 71 | $this->messageThread()->getHttpClient(), 72 | $this->pluck('sender.accountId') 73 | ); 74 | } 75 | 76 | /** 77 | * Creates a message based on the message type. 78 | */ 79 | public static function create(MessageThread $thread, object $messageData): AbstractMessage 80 | { 81 | switch (MessageType::tryFrom($messageData->messageType)) { 82 | case MessageType::Audio: 83 | return AudioMessage::fromObject($thread, $messageData); 84 | case MessageType::Image: 85 | return ImageMessage::fromObject($thread, $messageData); 86 | case MessageType::Video: 87 | return VideoMessage::fromObject($thread, $messageData); 88 | case MessageType::Sticker: 89 | return StickerMessage::fromObject($thread, $messageData); 90 | default: 91 | // We'll just default to a text message because there are certain types of messages (new voice chat, etc) that are basically text messages. 92 | return TextMessage::fromObject($thread, $messageData); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Factory/CloudMediaGalleryFactory.php: -------------------------------------------------------------------------------- 1 | npCommId) { 33 | throw new FilterException('Cannot filter by title id when a communication id filter is already set.'); 34 | } 35 | 36 | $this->titleId = $titleId; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Filters media based on NP communication id (trophy id). 43 | * 44 | * Cannot be paired with withTitleId. 45 | * 46 | * @param string $npCommId NPWRxxxxx_00 47 | * @return CloudMediaGalleryFactory 48 | * @throws FilterException 49 | */ 50 | public function withCommunicationId(string $npCommId): CloudMediaGalleryFactory 51 | { 52 | if ($this->title) { 53 | throw new FilterException('Cannot filter by communcation id when a title id filter is already set.'); 54 | } 55 | 56 | $this->npCommId = $npCommId; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Filters media based on it's transcoding status. 63 | * 64 | * Useful for filtering out any non-completed media. 65 | * 66 | * @param TranscodeStatusType $status 67 | * @return CloudMediaGalleryFactory 68 | */ 69 | public function withStatus(TranscodeStatusType $status): CloudMediaGalleryFactory 70 | { 71 | $this->transcodeStatus = $status; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Gets the iterator and applies any filters. 78 | * 79 | * @return Iterator 80 | */ 81 | public function getIterator(): Iterator 82 | { 83 | $iterator = new CloudMediaGalleryIterator($this); 84 | 85 | if ($this->titleId) { 86 | $iterator = new TitleIdFilter($iterator, $this->titleId); 87 | } 88 | 89 | // @TODO: Implement other filters. 90 | 91 | return $iterator; 92 | } 93 | 94 | /** 95 | * Gets the first media asset in the collection. 96 | * 97 | * @return Media 98 | */ 99 | public function first(): Media 100 | { 101 | return $this->getIterator()->current(); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Model/Store/Concept.php: -------------------------------------------------------------------------------- 1 | conceptId); 22 | $instance->setCache($data); 23 | 24 | return $instance; 25 | } 26 | 27 | /** 28 | * Gets the concept's product id. 29 | */ 30 | public function productId(): string 31 | { 32 | return $this->pluck('id'); 33 | } 34 | 35 | /** 36 | * Gets the concept's name. 37 | */ 38 | public function name(): string 39 | { 40 | return $this->pluck('name'); 41 | } 42 | 43 | /** 44 | * Gets the concept's id. 45 | */ 46 | public function conceptId(): string 47 | { 48 | return $this->conceptId; 49 | } 50 | 51 | /** 52 | * Gets the concept's publicher. 53 | */ 54 | public function publisher(): string 55 | { 56 | return ($this->pluck('publisherName') ?? $this->pluck('leadPublisherName')); 57 | } 58 | 59 | /** 60 | * Gets the concept's release date. 61 | */ 62 | public function releaseDate(): \DateTime 63 | { 64 | return Carbon::parse($this->pluck('releaseDate.value')); 65 | } 66 | 67 | /** 68 | * Gets a list of the concept's genres. 69 | * 70 | * @return array 71 | */ 72 | public function genres(): array 73 | { 74 | $genres = []; 75 | 76 | foreach ($this->pluck('combinedLocalizedGenres') ?? [] as $genre) 77 | { 78 | $genres[] = $genre['value']; 79 | } 80 | 81 | return $genres; 82 | } 83 | 84 | /** 85 | * Gets the concept's long-form description. 86 | */ 87 | public function longDescription(): string 88 | { 89 | return $this->descriptionByType(DescriptionType::Long); 90 | } 91 | 92 | /** 93 | * Gets the concept's short-form description. 94 | */ 95 | public function shortDescription(): string 96 | { 97 | return $this->descriptionByType(DescriptionType::Short); 98 | } 99 | 100 | /** 101 | * Gets a specific description by type. 102 | */ 103 | public function descriptionByType(DescriptionType $type): string 104 | { 105 | foreach ($this->pluck('descriptions') as $description) 106 | { 107 | if ($description['type'] === $type->value) 108 | { 109 | return $description['value']; 110 | } 111 | } 112 | 113 | return ''; 114 | } 115 | 116 | /** 117 | * Fetches the concept's information from the API. 118 | */ 119 | public function fetch(): object 120 | { 121 | return $this->graphql('metGetConceptById', [ 122 | 'conceptId' => $this->conceptId(), 123 | 'productId' => '' 124 | ])->conceptRetrieve; 125 | } 126 | } -------------------------------------------------------------------------------- /tests/Traits/OperandParserTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($parser->parseIt(2, 2)); 15 | $this->assertFalse($parser->parseIt(3, 2)); 16 | $this->assertFalse($parser->parseIt('2', 2)); 17 | 18 | $parser = new OperandParserTester('=='); 19 | $this->assertTrue($parser->parseIt(2, 2)); 20 | $this->assertFalse($parser->parseIt(3, 2)); 21 | $this->assertFalse($parser->parseIt('2', 2)); 22 | 23 | $parser = new OperandParserTester('==='); 24 | $this->assertTrue($parser->parseIt(2, 2)); 25 | $this->assertFalse($parser->parseIt(3, 2)); 26 | $this->assertFalse($parser->parseIt('2', 2)); 27 | } 28 | 29 | public function testItShouldParseNotEquals(): void 30 | { 31 | $parser = new OperandParserTester('<>'); 32 | $this->assertFalse($parser->parseIt(2, 2)); 33 | $this->assertTrue($parser->parseIt(3, 2)); 34 | $this->assertTrue($parser->parseIt('2', 2)); 35 | 36 | $parser = new OperandParserTester('!='); 37 | $this->assertFalse($parser->parseIt(2, 2)); 38 | $this->assertTrue($parser->parseIt(3, 2)); 39 | $this->assertTrue($parser->parseIt('2', 2)); 40 | } 41 | 42 | public function testItShouldParseBiggerThan(): void 43 | { 44 | $parser = new OperandParserTester('>'); 45 | $this->assertTrue($parser->parseIt(3, 2)); 46 | $this->assertTrue($parser->parseIt('b', 'a')); 47 | $this->assertFalse($parser->parseIt(2, 2)); 48 | $this->assertFalse($parser->parseIt(1, 2)); 49 | } 50 | 51 | public function testItShouldParseBiggerThanOrEquals(): void 52 | { 53 | $parser = new OperandParserTester('>='); 54 | $this->assertTrue($parser->parseIt(3, 2)); 55 | $this->assertTrue($parser->parseIt('b', 'a')); 56 | $this->assertTrue($parser->parseIt('a', 'a')); 57 | $this->assertTrue($parser->parseIt(2, 2)); 58 | $this->assertFalse($parser->parseIt(1, 2)); 59 | } 60 | 61 | public function testItShouldParseSmallerThan(): void 62 | { 63 | $parser = new OperandParserTester('<'); 64 | $this->assertFalse($parser->parseIt(3, 2)); 65 | $this->assertFalse($parser->parseIt('b', 'a')); 66 | $this->assertFalse($parser->parseIt(2, 2)); 67 | $this->assertTrue($parser->parseIt(1, 2)); 68 | } 69 | 70 | public function testItShouldParseSmallerThanOrEquals(): void 71 | { 72 | $parser = new OperandParserTester('<='); 73 | $this->assertFalse($parser->parseIt(3, 2)); 74 | $this->assertFalse($parser->parseIt('b', 'a')); 75 | $this->assertTrue($parser->parseIt('a', 'a')); 76 | $this->assertTrue($parser->parseIt(2, 2)); 77 | $this->assertTrue($parser->parseIt(1, 2)); 78 | } 79 | 80 | public function testItShouldThrowOnInvalidOperator(): void 81 | { 82 | $this->expectException(\RuntimeException::class); 83 | $this->expectExceptionMessage("Operator value [!!] is not supported."); 84 | 85 | $parser = new OperandParserTester('!!'); 86 | $parser->parseIt(3, 2); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | hasFetched = true; 37 | 38 | return $this->fetch(); 39 | } 40 | 41 | public function __construct(Client $client) 42 | { 43 | parent::__construct($client); 44 | } 45 | 46 | /** 47 | * Plucks an API property from the cache. Will populate cache if necessary. 48 | */ 49 | public function pluck(string $property, bool $ignoreCache = false): mixed 50 | { 51 | $pieces = explode('.', $property); 52 | 53 | $root = $pieces[0]; 54 | 55 | $exists = array_key_exists($root, $this->cache); 56 | 57 | if (!$exists) 58 | { 59 | if (!$this->hasFetched() || $ignoreCache) 60 | { 61 | $this->setCache($this->performFetch()); 62 | return $this->pluck($property); 63 | } 64 | else 65 | { 66 | return null; 67 | } 68 | } 69 | 70 | if (empty($this->cache)) { 71 | throw new \InvalidArgumentException('Failed to populate cache for model [' . get_class($this) . ']'); 72 | } 73 | 74 | $value = $this->cache[$root]; 75 | 76 | array_shift($pieces); 77 | 78 | foreach ($pieces as $piece) { 79 | if (!is_array($value)) { 80 | throw new \RuntimeException("Value [$value] passed to pluck is not an array, but tried accessing a key from it."); 81 | } 82 | 83 | $value = $value[$piece]; 84 | } 85 | 86 | return $value; 87 | } 88 | 89 | /** 90 | * Checks if data has been fetched from the API. 91 | */ 92 | protected function hasFetched(): bool 93 | { 94 | return $this->hasFetched; 95 | } 96 | 97 | /** 98 | * Sets the cache property. 99 | */ 100 | public function setCache(object $data): void 101 | { 102 | // So this is bad and probably slow, but it's less annoying than some recursive method. 103 | $this->cache = json_decode(json_encode($data, JSON_FORCE_OBJECT), true); 104 | } 105 | 106 | /** 107 | * Gets the current model's cache. 108 | */ 109 | public function getCache(): array 110 | { 111 | return $this->cache; 112 | } 113 | 114 | /** 115 | * Gets the factory for this model. 116 | */ 117 | public function getFactory(): FactoryInterface 118 | { 119 | return $this->factory; 120 | } 121 | 122 | /** 123 | * Sets the factory for this model 124 | */ 125 | public function setFactory(FactoryInterface $factory): void 126 | { 127 | $this->factory = $factory; 128 | } 129 | } -------------------------------------------------------------------------------- /tests/ApiTest.php: -------------------------------------------------------------------------------- 1 | getHashMap() as $op => $hash) { 21 | $this->client = $this->createMock(Client::class); 22 | $this->api = new Api($this->client); 23 | 24 | $variables = []; 25 | $response = new Response(200, [], '{"data": "some-data-' . $op . '"}'); 26 | 27 | $this->client 28 | ->expects($this->once()) 29 | ->method('get') 30 | ->with('graphql/v1/op', [ 31 | 'query' => [ 32 | 'operationName' => $op, 33 | 'variables' => json_encode($variables), 34 | 'extensions' => json_encode([ 35 | 'persistedQuery' => [ 36 | 'version' => 1, 37 | 'sha256Hash' => $hash, 38 | ], 39 | ]), 40 | ], 41 | 'headers' => [], 42 | ]) 43 | ->willReturn($response->withBody(new JsonStream($response->getBody()))); 44 | 45 | $this->assertEquals('some-data-' . $op, $this->api->graphql($op, $variables)); 46 | } 47 | } 48 | 49 | public function testItShouldThrowWithInvalidOperation(): void 50 | { 51 | $this->client = $this->createMock(Client::class); 52 | $this->api = new Api($this->client); 53 | 54 | $variables = []; 55 | $op = 'invalid-operation'; 56 | 57 | $this->client 58 | ->expects($this->never()) 59 | ->method('get'); 60 | 61 | $this->expectException(UnmappedGraphQLOperationException::class); 62 | $this->expectExceptionMessage('The GraphQL operation ' . $op . ' is not mapped.'); 63 | $this->api->graphql($op, $variables); 64 | } 65 | 66 | private function getHashMap(): array 67 | { 68 | return [ 69 | 'metGetConceptByProductIdQuery' => '0a4c9f3693b3604df1c8341fdc3e481f42eeecf961a996baaa65e65a657a6433', 70 | 'metGetConceptById' => 'cc90404ac049d935afbd9968aef523da2b6723abfb9d586e5f77ebf7c5289006', 71 | 'metGetProductById' => 'a128042177bd93dd831164103d53b73ef790d56f51dae647064cb8f9d9fc9d1a', 72 | 'metGetAddOnsByTitleId' => 'e98d01ff5c1854409a405a5f79b5a9bcd36a5c0679fb33f4e18113c157d4d916', 73 | 'metGetCategoryGrid' => 'b67a9e4414b80d8d762bf12a588c6125467ae0bb3bbe3cee3f7696c6984f8ef6', 74 | 'metGetCategoryGrids' => 'cc0b6513521c59a321bf62334fa23a92f22cd2ce1abe9f014fadac6379e414a8', 75 | 'metGetCategoryStrands' => '55ab5f168bec56f8362b5519f59faaf786d4e1cfeabb8bc969d6a65545e14f4d', 76 | 'metGetDefaultView' => 'bec1b8a3b0bae8c08e3ce2c7fe2f38a69343434ccfbcdd82cc1f2e44f86b7c40', 77 | 'metGetPricingDataByConceptId' => 'abcb311ea830e679fe2b697a27f755764535d825b24510ab1239a4ca3092bd09', 78 | 'metGetStoreWishlist' => '571149e8aa4d76af7dd33b92e1d6f8f828ebc5fa8f0f6bf51a8324a0e6d71324', 79 | 'metGetViews' => '6fd98ff7fecb603006fb5d92db176d5028435be163c8d1ee9f7c598ab4677dd1', 80 | 'metGetWebCheckoutCart' => '2d4165c4de76877a32f3d08c91ce2af0e01d69300131fed0a8022868235e85b1', 81 | 'metGetExperience' => '054e61ee68bbeadc21435caebcc4f2bba0919a99b06629d141b0b82dc55f10c4', 82 | ]; 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/Model/Media.php: -------------------------------------------------------------------------------- 1 | sourceUgcId); 25 | $media->setCache($data); 26 | 27 | return $media; 28 | } 29 | 30 | public function creator(): User 31 | { 32 | return new User($this->getHttpClient(), $this->pluck('sceUserAccountId')); 33 | } 34 | 35 | public function trophyTitle(): TrophyTitle 36 | { 37 | return new TrophyTitle($this->getHttpClient(), $this->npCommunicationId()); 38 | } 39 | 40 | public function game(): GameTitle 41 | { 42 | return new GameTitle($this->getHttpClient(), $this->titleId()); 43 | } 44 | 45 | public function id(): string 46 | { 47 | return $this->pluck('id'); 48 | } 49 | 50 | public function spoiler(): bool 51 | { 52 | return $this->pluck('isSpoiler'); 53 | } 54 | 55 | public function language(): string 56 | { 57 | return $this->pluck('language'); 58 | } 59 | 60 | public function type(): UgcType 61 | { 62 | return UgcType::from($this->pluck('ugcType')); 63 | } 64 | 65 | public function title(): string 66 | { 67 | return $this->pluck('title'); 68 | } 69 | 70 | public function uploadDate(): Carbon 71 | { 72 | return Carbon::parse($this->pluck('uploadDate')); 73 | } 74 | 75 | public function npCommunicationId(): string 76 | { 77 | return $this->pluck('npCommId'); 78 | } 79 | 80 | public function titleName(): string 81 | { 82 | return $this->pluck('sceTitleName'); 83 | } 84 | 85 | public function titleId(): string 86 | { 87 | return $this->pluck('sceTitleId'); 88 | } 89 | 90 | public function fileSize(): int 91 | { 92 | return $this->pluck('fileSize'); 93 | } 94 | 95 | public function fileType(): string 96 | { 97 | return $this->pluck('fileType'); 98 | } 99 | 100 | public function cloudStatus(): CloudStatusType 101 | { 102 | return CloudStatusType::from($this->pluck('cloudStatus')); 103 | } 104 | 105 | public function transcodeStatus(): TranscodeStatusType 106 | { 107 | return TranscodeStatusType::from($this->pluck('transcodeStatus')); 108 | } 109 | 110 | /** 111 | * Generates a URL with the required parameters to access the asset. 112 | 113 | * @return string 114 | */ 115 | public function url(): string 116 | { 117 | switch ($this->type()) 118 | { 119 | case UgcType::Video: 120 | return $this->generateUrls()->downloadUrl; 121 | break; 122 | 123 | case UgcType::Image: 124 | return $this->generateUrls()->screenshotUrl; 125 | break; 126 | } 127 | } 128 | 129 | /** 130 | * Generates parameterized URLs for the media asset. 131 | * 132 | * @return object 133 | */ 134 | private function generateUrls(): object 135 | { 136 | return $this->get('gameMediaService/v2/c2s/ugc/' . $this->id() . '/url'); 137 | } 138 | 139 | public function fetch(): object 140 | { 141 | return $this->get('gameMediaService/v2/c2s/content', [ 142 | 'fields' => implode(',', [ 143 | 'title', 144 | 'description', 145 | 'broadcastDate', 146 | 'sceTitleName', 147 | 'countOfViewers', 148 | 'sceUserOnlineId', 149 | 'streamingPreviewImage', 150 | 'serviceType', 151 | 'channelId', 152 | 'sceTitleId', 153 | 'isSpoiler', 154 | 'transcodeStatus' 155 | ]), 156 | 'ugcIds' => $this->ugcId 157 | ]); 158 | } 159 | } -------------------------------------------------------------------------------- /src/Model/GameTitle.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 14 | } 15 | 16 | /** 17 | * Creates a new game title from existing data. 18 | */ 19 | public static function fromObject(GameListFactory $gameListFactory, object $data): self 20 | { 21 | $game = new static($gameListFactory, $data->titleId); 22 | $game->setCache($data); 23 | 24 | return $game; 25 | } 26 | 27 | /** 28 | * Gets the store concept for the game. 29 | */ 30 | public function concept(): Concept 31 | { 32 | return new Concept($this->getHttpClient(), $this->pluck('concept.id')); 33 | } 34 | 35 | /** 36 | * Gets the play duration. 37 | * Example: "PT1192H44M48S" 38 | */ 39 | public function playDuration(): string 40 | { 41 | return $this->pluck('playDuration'); 42 | } 43 | 44 | /** 45 | * Gets the first played date. 46 | * Example: "2015-11-13T13:05:52Z" 47 | */ 48 | public function firstPlayedDateTime(): string 49 | { 50 | return $this->pluck('firstPlayedDateTime'); 51 | } 52 | 53 | /** 54 | * Gets the last played date. 55 | * Example: "2021-02-16T21:39:53.890Z" 56 | */ 57 | public function lastPlayedDateTime(): string 58 | { 59 | return $this->pluck('lastPlayedDateTime'); 60 | } 61 | 62 | /** 63 | * Gets the last play count. 64 | */ 65 | public function playCount(): int 66 | { 67 | return $this->pluck('playCount'); 68 | } 69 | 70 | /** 71 | * Gets the category. 72 | * Example: "ps4_game", "ps4_nongame_mini_app", "ps5_native_game", "unknown" 73 | */ 74 | public function category(): string 75 | { 76 | return $this->pluck('category'); 77 | } 78 | 79 | /** 80 | * Gets the localized image url. 81 | */ 82 | public function localizedImageUrl(): string 83 | { 84 | return $this->pluck('localizedImageUrl'); 85 | } 86 | 87 | /** 88 | * Gets the image url. 89 | */ 90 | public function imageUrl(): string 91 | { 92 | return $this->pluck('imageUrl'); 93 | } 94 | 95 | /** 96 | * Gets the localized name. 97 | */ 98 | public function localizedName(): string 99 | { 100 | return $this->pluck('localizedName'); 101 | } 102 | 103 | /** 104 | * Gets the name. 105 | */ 106 | public function name(): string 107 | { 108 | return $this->pluck('name'); 109 | } 110 | 111 | /** 112 | * Gets the id. 113 | * Example: "CUSA02818_00" 114 | */ 115 | public function id(): string 116 | { 117 | return $this->id ??= $this->pluck('titleId'); 118 | } 119 | 120 | /** 121 | * Gets the service. 122 | * Example: "none(purchased)", "none_purchased", "other" 123 | */ 124 | public function service(): string 125 | { 126 | return $this->pluck('service'); 127 | } 128 | 129 | /** 130 | * Gets the stats. 131 | */ 132 | public function stats(): object 133 | { 134 | return $this->pluck('stats'); 135 | } 136 | 137 | /** 138 | * Gets the media. 139 | */ 140 | public function media(): object 141 | { 142 | return $this->pluck('media'); 143 | } 144 | 145 | /** 146 | * Gets the game title data. 147 | */ 148 | public function fetch(): object 149 | { 150 | return $this->get('gamelist/v2/users/' . $this->getFactory()->getUser()->accountId() . '/titles/' . $this->id()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Factory/GroupsFactory.php: -------------------------------------------------------------------------------- 1 | with = array_merge($this->with, $onlineIds); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Should be used with the GroupsFactory::with method. 42 | * 43 | * Will return groups that contain ONLY the users passed to GroupsFactory::with. 44 | * 45 | * @return GroupsFactory 46 | */ 47 | public function only(): GroupsFactory 48 | { 49 | $this->only = true; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Filters groups that have only been active since the given date. 56 | * 57 | * @param Carbon $date 58 | * @return GroupsFactory 59 | */ 60 | public function since(Carbon $date): GroupsFactory 61 | { 62 | $this->since = $date; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Filters groups that are favorited. 69 | * 70 | * @return GroupsFactory 71 | */ 72 | public function favorited(): GroupsFactory 73 | { 74 | $this->favorited = true; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Gets the iterator and applies any filters. 81 | * 82 | * @return Iterator 83 | */ 84 | public function getIterator(): Iterator 85 | { 86 | $iterator = new GroupsIterator($this); 87 | 88 | if ($this->with) { 89 | $iterator = new GroupMembersFilter($iterator, $this->with, $this->only); 90 | } 91 | 92 | return $iterator; 93 | } 94 | 95 | /** 96 | * Gets the first group in the collection. 97 | * 98 | * @return Group 99 | */ 100 | public function first(): Group 101 | { 102 | return $this->getIterator()->current(); 103 | } 104 | 105 | /** 106 | * The date to get messages since then. 107 | * 108 | * Returns unix epoch if not set prior. 109 | * 110 | * @return Carbon 111 | */ 112 | public function getSinceDate(): Carbon 113 | { 114 | return $this->since ?? Carbon::createFromTimestamp(0); 115 | } 116 | 117 | /** 118 | * Creates a new message thread. 119 | * 120 | * Will return an existing message thread if a thread already exists containing the same users you pass to this method. 121 | * 122 | * @param User ...$users 123 | * @return MessageThread 124 | */ 125 | public function create(User ...$users): MessageThread 126 | { 127 | $invitees = []; 128 | 129 | foreach ($users as $user) { 130 | $invitees[] = ['accountId' => $user->accountId()]; // TODO: Test if onlineId can still be used here. 131 | } 132 | 133 | $response = $this->postJson('gamingLoungeGroups/v1/groups', [ 134 | 'invitees' => $invitees 135 | ]); 136 | 137 | return new MessageThread(new Group($this, $response->groupId), $response->mainThread->threadId); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Model/Group.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 19 | } 20 | 21 | /** 22 | * Creates a new group from existing data. 23 | */ 24 | public static function fromObject(GroupsFactory $groupsFactory, object $data): self 25 | { 26 | $instance = new static($groupsFactory, $data->groupId); 27 | $instance->setCache($data); 28 | 29 | return $instance; 30 | } 31 | 32 | /** 33 | * Gets all the members in the message thread. 34 | * 35 | * @return GroupMembersFactory 36 | */ 37 | public function members(): GroupMembersFactory 38 | { 39 | return new GroupMembersFactory($this); 40 | } 41 | 42 | /** 43 | * Gets all the message thread members as an array. 44 | * 45 | * @return array 46 | */ 47 | public function membersArray(): array 48 | { 49 | return $this->pluck('members'); 50 | } 51 | 52 | /** 53 | * Gets the member count in the message thread. 54 | * 55 | * @return integer 56 | */ 57 | public function memberCount(): int 58 | { 59 | return count( 60 | $this->members() 61 | ); 62 | } 63 | 64 | /** 65 | * The date and time when the client joined the group. 66 | * 67 | * @return Carbon 68 | */ 69 | public function joined(): Carbon 70 | { 71 | return Carbon::parse($this->pluck('joinedTimestamp')); 72 | } 73 | 74 | /** 75 | * Gets if the group is favorited or not. 76 | * 77 | * @return boolean 78 | */ 79 | public function isFavorited(): bool 80 | { 81 | return $this->pluck('isFavorited'); 82 | } 83 | 84 | public function favorite() 85 | { 86 | // @TODO: Add favorite code here. 87 | } 88 | 89 | /** 90 | * The main message thread for this group. 91 | * 92 | * @return MessageThread 93 | */ 94 | public function messageThread(): MessageThread 95 | { 96 | return MessageThread::fromObject($this, (object)$this->pluck('mainThread')); 97 | } 98 | 99 | public function partySessions() 100 | { 101 | // @TODO: Figure this one out... 102 | } 103 | 104 | /** 105 | * Sends a message to the group's message thread. 106 | * 107 | * @param Sendable $message 108 | * @return AbstractMessage 109 | */ 110 | public function sendMessage(Sendable $message): AbstractMessage 111 | { 112 | return $this->messageThread()->sendMessage($message); 113 | } 114 | 115 | /** 116 | * The group id. 117 | * 118 | * @return string 119 | */ 120 | public function id(): string 121 | { 122 | return $this->groupId; 123 | } 124 | 125 | /** 126 | * Gets the group info from the PlayStation API. 127 | * 128 | * @return object 129 | */ 130 | public function fetch(): object 131 | { 132 | return $this->get('gamingLoungeGroups/v1/members/me/groups/' . $this->id(), [ 133 | 'fields' => implode(',', [ 134 | 'groupName', 135 | 'groupIcon', 136 | 'members', 137 | 'mainThread', 138 | 'joinedTimestamp', 139 | 'modifiedTimestamp', 140 | 'isFavorite', 141 | 'existsNewArrival', 142 | 'partySession' 143 | ]) 144 | ]); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Model/Trophy/Trophy.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 13 | } 14 | 15 | /** 16 | * Creates a new trophy from existing data. 17 | */ 18 | public static function fromObject(TrophyGroup $trophyGroup, object $data): self 19 | { 20 | $trophy = new static($trophyGroup, $data->trophyId); 21 | $trophy->setCache($data); 22 | 23 | return $trophy; 24 | } 25 | /** 26 | * Gets the trophy name. 27 | */ 28 | public function name(): string 29 | { 30 | return $this->pluck('trophyName'); 31 | } 32 | 33 | /** 34 | * Gets the trophy id. 35 | */ 36 | public function id(): int 37 | { 38 | return $this->id ??= $this->pluck('id'); 39 | } 40 | 41 | /** 42 | * Gets the trophy details. 43 | */ 44 | public function detail(): string 45 | { 46 | return $this->pluck('trophyDetail'); 47 | } 48 | 49 | /** 50 | * Gets the trophy type. (platinum, bronze, silver, gold) 51 | */ 52 | public function type(): TrophyType 53 | { 54 | return TrophyType::from($this->pluck('trophyType')); 55 | } 56 | 57 | /** 58 | * Get the trophy earned rate. 59 | */ 60 | public function earnedRate(): float 61 | { 62 | return $this->pluck('trophyEarnedRate'); 63 | } 64 | 65 | /** 66 | * Check if the trophy is hidden. 67 | */ 68 | public function hidden(): bool 69 | { 70 | return $this->pluck('trophyHidden'); 71 | } 72 | 73 | /** 74 | * Gets the trophy icon URL. 75 | */ 76 | public function iconUrl(): string 77 | { 78 | return $this->pluck('trophyIconUrl'); 79 | } 80 | 81 | /** 82 | * Gets the trophy progress target value, if any. 83 | */ 84 | public function progressTargetValue(): string 85 | { 86 | return $this->pluck('trophyProgressTargetValue') ?? ''; 87 | } 88 | 89 | /** 90 | * Gets the trophy reward name, if any. 91 | * Examples: "Emote", "Profile Avatar", "Profile Banner" 92 | */ 93 | public function rewardName(): string 94 | { 95 | return $this->pluck('trophyRewardName') ?? ''; 96 | } 97 | 98 | /** 99 | * Gets the trophy reward image url, if any. 100 | */ 101 | public function rewardImageUrl(): string 102 | { 103 | return $this->pluck('trophyRewardImageUrl') ?? ''; 104 | } 105 | 106 | /** 107 | * Check if the user have earned this trophy. 108 | */ 109 | public function earned(): bool 110 | { 111 | return $this->pluck('earned'); 112 | } 113 | 114 | /** 115 | * Get the date and time the user earned this trophy, if any. 116 | */ 117 | public function earnedDateTime(): string 118 | { 119 | return $this->pluck('earnedDateTime') ?? ''; 120 | } 121 | 122 | /** 123 | * Get the progress count for the user on this trophy, if any. 124 | */ 125 | public function progress(): string 126 | { 127 | return $this->pluck('progress') ?? ''; 128 | } 129 | 130 | /** 131 | * Get the progress percentage for the user on this trophy, if any. 132 | */ 133 | public function progressRate(): string 134 | { 135 | return $this->pluck('progressRate') ?? ''; 136 | } 137 | 138 | /** 139 | * Get the date and time when a progress was made for the user on this trophy, if any. 140 | */ 141 | public function progressedDateTime(): string 142 | { 143 | return $this->pluck('progressedDateTime') ?? ''; 144 | } 145 | 146 | /** 147 | * Fetches the trophy data from the API. 148 | */ 149 | public function fetch(): object 150 | { 151 | return $this->get('trophy/v1/npCommunicationIds/' . $this->trophyGroup->title()->npCommunicationId() . '/trophies/' . $this->id(), [ 152 | 'npServiceName' => $this->trophyGroup->title()->serviceName() 153 | ]); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Factory/TrophyTitlesFactory.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 40 | 41 | $this->user = $user; 42 | } 43 | 44 | /** 45 | * Filters trophy titles that either have trophy groups or no trophy groups. 46 | * 47 | * @param boolean $value 48 | * @return TrophyTitlesFactory 49 | */ 50 | public function hasTrophyGroups(bool $value = true): TrophyTitlesFactory 51 | { 52 | $this->hasTrophyGroups = $value; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Filters trophy titles to only get titles containing the supplied name. 59 | * 60 | * @param string $name 61 | * @return TrophyTitlesFactory 62 | */ 63 | public function withName(string $name): TrophyTitlesFactory 64 | { 65 | $this->withName = $name; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Filters trophy titles to only get trophies for the specific user. 72 | * 73 | * @param User $user 74 | * @return TrophyTitlesFactory 75 | */ 76 | public function forUser(User $user): TrophyTitlesFactory 77 | { 78 | $this->user = $user; 79 | 80 | return $this; 81 | } 82 | /** 83 | * Gets the iterator and applies any filters. 84 | * 85 | * @return Iterator 86 | */ 87 | public function getIterator(): Iterator 88 | { 89 | $iterator = new TrophyTitlesIterator($this); 90 | 91 | if ($this->withName) { 92 | $iterator = new TrophyTitleNameFilter($iterator, $this->withName); 93 | } 94 | 95 | if (!is_null($this->hasTrophyGroups)) { 96 | $iterator = new TrophyTitleHasGroupsFilter($iterator, $this->hasTrophyGroups); 97 | } 98 | 99 | return $iterator; 100 | } 101 | 102 | /** 103 | * Gets the current user (if specified) to get trophies for. 104 | * 105 | * @return User|null 106 | */ 107 | public function getUser(): ?User 108 | { 109 | return $this->user; 110 | } 111 | 112 | /** 113 | * Checks to see if this factory should be looking at a specific user's trophies. 114 | * 115 | * @return boolean 116 | */ 117 | public function hasUser(): bool 118 | { 119 | return $this->user !== null; 120 | } 121 | 122 | /** 123 | * Gets the current platforms passed to this instance. 124 | * 125 | * @return array 126 | */ 127 | public function getPlatforms(): array 128 | { 129 | return $this->platforms; 130 | } 131 | 132 | /** 133 | * Gets the current language passed to this instance. 134 | * 135 | * If the language has not been set prior, this will return LanguageType::English. 136 | * 137 | * @return LanguageType 138 | */ 139 | public function getLanguage(): LanguageType 140 | { 141 | return $this->language ?? LanguageType::English; 142 | } 143 | 144 | /** 145 | * Gets the first trophy title in the collection. 146 | * 147 | * @return UserTrophyTitle 148 | */ 149 | public function first(): UserTrophyTitle 150 | { 151 | try { 152 | return $this->getIterator()->current(); 153 | } catch (InvalidArgumentException $e) { 154 | throw new NoTrophiesException("Client has no trophy titles."); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Iterator/AbstractApiIterator.php: -------------------------------------------------------------------------------- 1 | currentOffset; 37 | } 38 | 39 | /** 40 | * Gets the item count. 41 | */ 42 | public final function count(): int 43 | { 44 | return $this->getTotalResults(); 45 | } 46 | 47 | /** 48 | * Gets the total results. 49 | */ 50 | public function getTotalResults(): int 51 | { 52 | return $this->totalResults; 53 | } 54 | 55 | /** 56 | * Sets the total results. 57 | */ 58 | protected final function setTotalResults(int $results): void 59 | { 60 | $this->totalResults = $results; 61 | } 62 | 63 | /** 64 | * Checks if the current offset exists in the cache. 65 | */ 66 | public final function valid(): bool 67 | { 68 | return array_key_exists($this->currentOffset, $this->cache); 69 | } 70 | 71 | /** 72 | * Resets the iterator to the first item. 73 | */ 74 | public function rewind(): void 75 | { 76 | $this->currentOffset = 0; 77 | } 78 | 79 | /** 80 | * Updates the total result count and adds the new items onto the cache. 81 | */ 82 | public final function update(int $totalResults, array $items, mixed $customCursor = null): void 83 | { 84 | $this->setTotalResults($totalResults); 85 | 86 | $this->cache = array_merge($this->cache, $items); 87 | 88 | $this->customCursor = $customCursor; 89 | } 90 | 91 | /** 92 | * Set whether to force the iterator to keep accessing values or not. 93 | */ 94 | public function force(bool $value): void 95 | { 96 | $this->force = $value; 97 | } 98 | 99 | /** 100 | * Points the offset to the next item. 101 | * 102 | * Will request data from the API whenever necessary. 103 | */ 104 | public function next(): void 105 | { 106 | $this->currentOffset++; 107 | 108 | if (is_null($this->limit)) { 109 | return; 110 | } 111 | 112 | if ($this->currentOffset % $this->limit === 0 && $this->currentOffset < $this->getTotalResults()) { 113 | if ($this->customCursor) { 114 | $this->access($this->customCursor); 115 | } else { 116 | $this->access($this->currentOffset); 117 | } 118 | } 119 | } 120 | 121 | 122 | /** 123 | * Gets an item from cache, or from the API resource if necessary, by an offset. 124 | */ 125 | public function getFromOffset(mixed $offset): object 126 | { 127 | if (is_null($offset)) { 128 | throw new \InvalidArgumentException("Offset cannot be null."); 129 | } 130 | 131 | if (!$this->offsetExists($offset)) { 132 | throw new \InvalidArgumentException("Offset $offset does not exist."); 133 | } 134 | 135 | if (!array_key_exists($offset, $this->cache)) { 136 | $this->access($offset); 137 | } 138 | 139 | return $this->cache[$offset]; 140 | } 141 | 142 | /** 143 | * Ensures that an offset exists before trying to access it by an offset. 144 | */ 145 | public function offsetExists(mixed $offset): bool 146 | { 147 | try { 148 | return $offset >= 0 && $offset < $this->getTotalResults(); 149 | } catch (\RuntimeException $e) { 150 | return !$this->lastBlock; 151 | } 152 | } 153 | 154 | /** 155 | * Appends a new collection of items onto the cache. 156 | */ 157 | protected final function appendToCache(array $items): void 158 | { 159 | $this->cache = array_merge($this->cache, $items); 160 | } 161 | 162 | /** 163 | * Gets the first item in the iterator. 164 | */ 165 | public function first(): object 166 | { 167 | $this->rewind(); 168 | 169 | return $this->current(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Model/Trophy/TrophyGroup.php: -------------------------------------------------------------------------------- 1 | getHttpClient()); 19 | } 20 | 21 | /** 22 | * Creates a new trophy group from existing data. 23 | */ 24 | public static function fromObject(AbstractTrophyTitle $trophyTitle, object $data): self 25 | { 26 | $instance = new static($trophyTitle, $data->trophyGroupId, $data->trophyGroupName, $data->trophyGroupIconUrl, $data->trophyGroupDetail); 27 | $instance->setCache($data); 28 | 29 | return $instance; 30 | } 31 | 32 | /** 33 | * Gets the trophy title for this trophy group. 34 | */ 35 | public function title(): AbstractTrophyTitle 36 | { 37 | return $this->trophyTitle; 38 | } 39 | 40 | /** 41 | * Gets all the trophies in the trophy group. 42 | */ 43 | public function trophies(): TrophyFactory 44 | { 45 | return new TrophyFactory($this); 46 | } 47 | 48 | /** 49 | * Gets the trophy group name. 50 | */ 51 | public function name(): string 52 | { 53 | return $this->groupName; 54 | } 55 | 56 | /** 57 | * Gets the trophy group detail. 58 | */ 59 | public function detail(): string 60 | { 61 | return $this->groupDetail ?? ''; 62 | } 63 | 64 | /** 65 | * Gets the trophy group ID. 66 | */ 67 | public function id(): string 68 | { 69 | return $this->groupId; 70 | } 71 | 72 | /** 73 | * Gets the trophy group icon URL. 74 | */ 75 | public function iconUrl(): string 76 | { 77 | return $this->groupIconUrl; 78 | } 79 | 80 | /** 81 | * Gets the defined trophies for this trophy group. 82 | */ 83 | public function definedTrophies(): array 84 | { 85 | return $this->pluck('definedTrophies'); 86 | } 87 | 88 | /** 89 | * Gets the bronze trophy count. 90 | */ 91 | public function bronze(): int 92 | { 93 | return $this->pluck('definedTrophies.bronze'); 94 | } 95 | 96 | /** 97 | * Gets the silver trophy count. 98 | */ 99 | public function silver(): int 100 | { 101 | return $this->pluck('definedTrophies.silver'); 102 | } 103 | 104 | /** 105 | * Gets the gold trophy count. 106 | */ 107 | public function gold(): int 108 | { 109 | return $this->pluck('definedTrophies.gold'); 110 | } 111 | 112 | /** 113 | * Gets whether this trophy group has a platinum or not. 114 | */ 115 | public function hasPlatinum(): bool 116 | { 117 | return $this->pluck('definedTrophies.platinum') == 1; 118 | } 119 | 120 | /** 121 | * Gets the trophy count for a specificed trophy type. 122 | */ 123 | public function trophyCount(TrophyType $trophyType): int 124 | { 125 | switch ($trophyType) { 126 | case TrophyType::Bronze: 127 | return $this->bronze(); 128 | case TrophyType::Silver: 129 | return $this->silver(); 130 | case TrophyType::Gold: 131 | return $this->gold(); 132 | case TrophyType::Platinum: 133 | return (int)$this->hasPlatinum(); 134 | default: 135 | throw new \InvalidArgumentException("Trophy type [$trophyType] does not contain a count method."); 136 | } 137 | } 138 | 139 | /** 140 | * Gets the amount of trophies in the trophy group. 141 | */ 142 | public function totalTrophyCount(): int 143 | { 144 | $count = $this->bronze() + $this->silver() + $this->gold(); 145 | 146 | return $this->hasPlatinum() ? ++$count : $count; 147 | } 148 | 149 | /** 150 | * Fetches the trophy group information from the API. 151 | */ 152 | public function fetch(): object 153 | { 154 | if ($this->title() instanceof UserTrophyTitle) { 155 | return $this->get( 156 | 'trophy/v1/users/' . $this->title()->getFactory()->getUser()->accountId() . '/npCommunicationIds/' . $this->title()->npCommunicationId() . '/trophyGroups', 157 | [ 158 | 'npServiceName' => $this->title()->serviceName() 159 | ] 160 | ); 161 | } else { 162 | return $this->get( 163 | 'trophy/v1/npCommunicationIds/' . $this->title()->npCommunicationId() . '/trophyGroups', 164 | [ 165 | 'npServiceName' => $this->title()->serviceName() 166 | ] 167 | ); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Model/Trophy/UserTrophyTitle.php: -------------------------------------------------------------------------------- 1 | pluck('hasTrophyGroups'); 17 | } 18 | 19 | /** 20 | * Gets the name of the title. 21 | */ 22 | public function name(): string 23 | { 24 | return $this->pluck('trophyTitleName'); 25 | } 26 | 27 | /** 28 | * Gets the detail of the title. 29 | */ 30 | public function detail(): string 31 | { 32 | // PS5 titles don't seem to have the detail data. 33 | if ($this->serviceName() == 'trophy2') { 34 | return ''; 35 | } 36 | 37 | return $this->pluck('trophyTitleDetail'); 38 | } 39 | 40 | /** 41 | * Gets the icon URL for the title. 42 | */ 43 | public function iconUrl(): string 44 | { 45 | return $this->pluck('trophyTitleIconUrl'); 46 | } 47 | 48 | /** 49 | * Gets the platform(s) this title is for. 50 | * 51 | * @return array 52 | */ 53 | public function platform(): array 54 | { 55 | $platforms = []; 56 | 57 | foreach (explode(",", $this->pluck('trophyTitlePlatform')) as $platform) { 58 | $platforms[] = ConsoleType::tryFrom($platform); 59 | } 60 | 61 | return $platforms; 62 | } 63 | 64 | /** 65 | * Checks if this title has trophies. 66 | */ 67 | public function hasTrophies(): bool 68 | { 69 | $value = $this->pluck('definedTrophies'); 70 | 71 | return isset($value) && !empty($value); 72 | } 73 | 74 | /** 75 | * Checks if this title has a platinum trophy. 76 | */ 77 | public function hasPlatinum(): bool 78 | { 79 | return $this->pluck('definedTrophies.platinum') ?? false; 80 | } 81 | 82 | /** 83 | * Gets the total trophy count for this title. 84 | */ 85 | public function trophyCount(): int 86 | { 87 | $count = ($this->bronzeTrophyCount() + $this->silverTrophyCount() + $this->goldTrophyCount()); 88 | 89 | if ($this->hasPlatinum()) { 90 | $count++; 91 | } 92 | 93 | return $count; 94 | } 95 | 96 | /** 97 | * Gets the amount of bronze trophies. 98 | */ 99 | public function bronzeTrophyCount(): int 100 | { 101 | return $this->pluck('definedTrophies.bronze'); 102 | } 103 | 104 | /** 105 | * Gets the amount of silver trophies. 106 | */ 107 | public function silverTrophyCount(): int 108 | { 109 | return $this->pluck('definedTrophies.silver'); 110 | } 111 | 112 | /** 113 | * Gets the amount of gold trophies. 114 | */ 115 | public function goldTrophyCount(): int 116 | { 117 | return $this->pluck('definedTrophies.gold'); 118 | } 119 | 120 | /** 121 | * Gets the NP communication ID (NPWR_) for this trophy title. 122 | */ 123 | public function npCommunicationId(): string 124 | { 125 | return $this->pluck('npCommunicationId'); 126 | } 127 | 128 | /** 129 | * Gets the trophy list version number for this trophy title. 130 | */ 131 | public function trophySetVersion(): string 132 | { 133 | return $this->pluck('trophySetVersion'); 134 | } 135 | 136 | /** 137 | * Gets the last updated date and time for the trophy title for this user. 138 | */ 139 | public function lastUpdatedDateTime(): string 140 | { 141 | return $this->pluck('lastUpdatedDateTime'); 142 | } 143 | 144 | /** 145 | * Gets the amount of earned bronze trophies for this user. 146 | */ 147 | public function earnedTrophiesBronzeCount(): int 148 | { 149 | return $this->pluck('earnedTrophies.bronze'); 150 | } 151 | 152 | /** 153 | * Gets the amount of earned silver trophies for this user. 154 | */ 155 | public function earnedTrophiesSilverCount(): int 156 | { 157 | return $this->pluck('earnedTrophies.silver'); 158 | } 159 | 160 | /** 161 | * Gets the amount of earned gold trophies for this user. 162 | */ 163 | public function earnedTrophiesGoldCount(): int 164 | { 165 | return $this->pluck('earnedTrophies.gold'); 166 | } 167 | 168 | /** 169 | * Gets the amount of earned platinum trophies for this user. 170 | */ 171 | public function earnedTrophiesPlatinumCount(): int 172 | { 173 | return $this->pluck('earnedTrophies.platinum'); 174 | } 175 | 176 | /** 177 | * Gets the trophy title progress percent for this user. 178 | */ 179 | public function progress(): int 180 | { 181 | return $this->pluck('progress'); 182 | } 183 | 184 | /** 185 | * Gets the trophy service name for this trophy. 186 | */ 187 | public function serviceName(): string 188 | { 189 | return $this->serviceName ??= $this->pluck('npServiceName'); 190 | } 191 | 192 | // @TODO: Implement 193 | public function fetch(): object 194 | { 195 | throw new \BadMethodCallException(); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Model/User.php: -------------------------------------------------------------------------------- 1 | accountId); 30 | $instance->setCache($data); 31 | 32 | return $instance; 33 | } 34 | 35 | /** 36 | * Sets the country for this user. 37 | */ 38 | public function setCountry(string $country): self 39 | { 40 | $this->country = $country; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Get the trophy titles associated with this user's account. 47 | * 48 | * @return TrophyTitlesFactory 49 | */ 50 | public function trophyTitles(): TrophyTitlesFactory 51 | { 52 | return new TrophyTitlesFactory($this); 53 | } 54 | 55 | /** 56 | * Get the game list for this user's account. 57 | * 58 | * @return GameListFactory 59 | */ 60 | public function gameList(): GameListFactory 61 | { 62 | return new GameListFactory($this); 63 | } 64 | 65 | /** 66 | * Gets the user's friends list. 67 | * 68 | * @return FriendsListFactory 69 | */ 70 | public function friends(): FriendsListFactory 71 | { 72 | return new FriendsListFactory($this); 73 | } 74 | 75 | /** 76 | * Get the communication id (NPWR...) from a title id (CUSA...) 77 | * 78 | * Only works for PS4/PS5 titles. 79 | * Doesn't work with PPSA... title ids. 80 | */ 81 | public function titleIdToCommunicationId($npTitleId): string 82 | { 83 | $body = [ 84 | 'npTitleIds' => $npTitleId 85 | ]; 86 | 87 | $results = $this->get('trophy/v1/users/' . $this->accountId() . '/titles/trophyTitles', $body); 88 | 89 | if (count($results->titles[0]->trophyTitles) == 0) { 90 | return ''; 91 | } 92 | 93 | return $results->titles[0]->trophyTitles[0]->npCommunicationId; 94 | } 95 | 96 | /** 97 | * Gets the trophy summary for the user. 98 | */ 99 | public function trophySummary(): TrophySummary 100 | { 101 | return new TrophySummary($this); 102 | } 103 | 104 | /** 105 | * Gets online ID. 106 | */ 107 | public function onlineId(): string 108 | { 109 | return $this->pluck('onlineId'); 110 | } 111 | 112 | /** 113 | * Gets the about me. 114 | */ 115 | public function aboutMe(): string 116 | { 117 | return $this->pluck('aboutMe'); 118 | } 119 | 120 | /** 121 | * Gets the user's account ID. 122 | */ 123 | public function accountId(): string 124 | { 125 | return $this->accountId; 126 | } 127 | 128 | /** 129 | * Gets the user's country. 130 | * 131 | * This property will only be available if the user was obtained via the user search endpoint. 132 | */ 133 | public function country(): ?string 134 | { 135 | return $this->country; 136 | } 137 | 138 | /** 139 | * Returns all the available avatar URL sizes. 140 | * 141 | * Each array key is the size of the image. 142 | */ 143 | public function avatarUrls(): array 144 | { 145 | $urls = []; 146 | 147 | foreach ($this->pluck('avatars') as $avatar) { 148 | $urls[$avatar['size']] = $avatar['url']; 149 | } 150 | 151 | return $urls; 152 | } 153 | 154 | /** 155 | * Gets the avatar URL. 156 | * 157 | * This should return the largest size available. 158 | */ 159 | public function avatarUrl(): string 160 | { 161 | $sizes = ['xl', 'l', 'm', 's']; 162 | 163 | foreach ($sizes as $size) { 164 | if (array_key_exists($size, $this->avatarUrls())) { 165 | return $this->avatarUrls()[$size]; 166 | } 167 | } 168 | 169 | // Could not find any of the sizes specified, just return the first one in the array. 170 | return current($this->avatarUrls()); 171 | } 172 | 173 | /** 174 | * Check if client is blocking the user. 175 | */ 176 | public function isBlocking(): bool 177 | { 178 | return $this->pluck('blocking'); 179 | } 180 | 181 | /** 182 | * Get the user's follower count. 183 | */ 184 | public function followerCount(): int 185 | { 186 | return $this->pluck('followerCount'); 187 | } 188 | 189 | /** 190 | * Check if the client is following the user. 191 | */ 192 | public function isFollowing(): bool 193 | { 194 | return $this->pluck('following'); 195 | } 196 | 197 | /** 198 | * Check if the user is verified. 199 | */ 200 | public function isVerified(): bool 201 | { 202 | return $this->pluck('isOfficiallyVerified'); 203 | } 204 | 205 | /** 206 | * Gets all the user's languages. 207 | */ 208 | public function languages(): array 209 | { 210 | return $this->pluck('languagesUsed'); 211 | } 212 | 213 | /** 214 | * Gets mutual friend count. 215 | * 216 | * Returns -1 if current profile is the logged in user. 217 | */ 218 | public function mutualFriendCount(): int 219 | { 220 | return $this->pluck('mutualFriendsCount'); 221 | } 222 | 223 | /** 224 | * Checks if the client has any mutual friends with the user. 225 | */ 226 | public function hasMutualFriends(): bool 227 | { 228 | return $this->mutualFriendCount() > 0; 229 | } 230 | 231 | /** 232 | * Checks if the client is close friends with the user. 233 | */ 234 | public function isCloseFriend(): bool 235 | { 236 | return $this->pluck('personalDetail') !== null; 237 | } 238 | 239 | /** 240 | * Checks if the client has a pending friend request with the user. 241 | * 242 | * @TODO: Check if this works both ways. 243 | */ 244 | public function hasFriendRequested(): bool 245 | { 246 | return $this->pluck('friendRelation') === 'requesting'; 247 | } 248 | 249 | /** 250 | * Checks if the user is currently online. 251 | */ 252 | public function isOnline(): bool 253 | { 254 | return $this->pluck('presences.0.onlineStatus') === 'online'; 255 | } 256 | 257 | /** 258 | * Checks if the user has PlayStation Plus. 259 | */ 260 | public function hasPlus(): bool 261 | { 262 | return $this->pluck('isPlus'); 263 | } 264 | 265 | /** 266 | * Fetches the user's profile information from the API. 267 | */ 268 | public function fetch(): object 269 | { 270 | return $this->get('userProfile/v1/internal/users/' . $this->accountId . '/profiles'); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | get(self::AUTH_URL . 'authz/v3/oauth/authorize', [ 46 | 'access_type' => 'offline', 47 | 'app_context' => 'inapp_ios', 48 | 'auth_ver' => 'v3', 49 | 'cid' => '60351282-8C5F-4D5E-9033-E48FEA973E11', 50 | 'client_id' => '09515159-7237-4370-9b40-3806e67c0891', 51 | 'darkmode' => 'true', 52 | 'device_base_font_size' => 10, 53 | 'device_profile' => 'mobile', 54 | 'duid' => '0000000d0004008088347AA0C79542D3B656EBB51CE3EBE1', 55 | 'elements_visibility' => 'no_aclink', 56 | 'extraQueryParams' => '{ 57 | PlatformPrivacyWs1 = minimal; 58 | }', 59 | 'no_captcha' => 'true', 60 | 'redirect_uri' => 'com.scee.psxandroid.scecompcall://redirect', 61 | 'response_type' => 'code', 62 | 'scope' => 'psn:mobile.v2.core psn:clientapp', 63 | 'service_entity' => 'urn:service-entity:psn', 64 | 'service_logo' => 'ps', 65 | 'smcid' => 'psapp:settings-entrance', 66 | 'support_scheme' => 'sneiprls', 67 | 'token_format' => 'jwt', 68 | 'ui' => 'pr', 69 | ], [ 70 | 'Cookie' => 'npsso=' . $npsso 71 | ]); 72 | 73 | $lastResponse = $this->getLastResponse(); 74 | 75 | if ($lastResponse->getStatusCode() !== 302) { 76 | throw new \Exception('Incorrect response code from oauth/authorize.'); 77 | } 78 | 79 | $location = $lastResponse->getHeaderLine('Location'); 80 | 81 | if (!$location) { 82 | throw new \Exception('Missing redirect location from oauth/authorize.'); 83 | } 84 | 85 | $parsedUrl = parse_url($location, PHP_URL_QUERY); 86 | 87 | if ($parsedUrl === null) { 88 | throw new \Exception('Failed parsing location header'); 89 | } 90 | 91 | parse_str($parsedUrl, $params); 92 | 93 | if (!array_key_exists('code', $params)) { 94 | throw new \Exception('Missing code from oauth/authorize.'); 95 | } 96 | 97 | $response = $this->post(self::AUTH_URL . 'authz/v3/oauth/token', [ 98 | 'smcid' => 'psapp%3Asettings-entrance', 99 | 'access_type' => 'offline', 100 | 'code' => $params['code'], 101 | 'service_logo' => 'ps', 102 | 'ui' => 'pr', 103 | 'elements_visibility' => 'no_aclink', 104 | 'redirect_uri' => 'com.scee.psxandroid.scecompcall://redirect', 105 | 'support_scheme' => 'sneiprls', 106 | 'grant_type' => 'authorization_code', 107 | 'darkmode' => 'true', 108 | 'device_base_font_size' => 10, 109 | 'device_profile' => 'mobile', 110 | 'app_context' => 'inapp_ios', 111 | 'extraQueryParams' => '{ 112 | PlatformPrivacyWs1 = minimal; 113 | }', 114 | 'token_format' => 'jwt' 115 | ], [ 116 | 'Cookie' => 'npsso=' . $npsso, 117 | 'Authorization' => 'Basic MDk1MTUxNTktNzIzNy00MzcwLTliNDAtMzgwNmU2N2MwODkxOnVjUGprYTV0bnRCMktxc1A=', 118 | ]); 119 | 120 | $this->finalizeLogin($response); 121 | } 122 | 123 | /** 124 | * Login with an existing refresh token. 125 | * 126 | * @see https://tusticles.com/psn-php/future_logins.html 127 | * 128 | * @param string $refreshToken 129 | * @return void 130 | */ 131 | public function loginWithRefreshToken(string $refreshToken) 132 | { 133 | // @TODO: Handle errors. 134 | $response = $this->post('authz/v3/oauth/token', [ 135 | 'scope' => 'psn:mobile.v2.core psn:clientapp', 136 | 'refresh_token' => $refreshToken, 137 | 'grant_type' => 'refresh_token', 138 | 'token_format' => 'jwt', 139 | ], ['Authorization' => 'Basic MDk1MTUxNTktNzIzNy00MzcwLTliNDAtMzgwNmU2N2MwODkxOnVjUGprYTV0bnRCMktxc1A=']); 140 | 141 | $this->finalizeLogin($response); 142 | } 143 | 144 | /** 145 | * Finishes the login flow and sets up future request middleware. 146 | * 147 | * @param object $response 148 | * @return void 149 | */ 150 | private function finalizeLogin(object $response) 151 | { 152 | $this->accessToken = new OAuthToken($response->access_token, $response->expires_in); 153 | $this->refreshToken = new OAuthToken($response->refresh_token, $response->refresh_token_expires_in); 154 | 155 | $this->pushAuthenticationMiddleware(new AuthenticationMiddleware([ 156 | 'Authorization' => 'Bearer ' . $this->getAccessToken()->getToken(), 157 | ])); 158 | } 159 | 160 | /** 161 | * Access the PlayStation API using an existing access token. 162 | * 163 | * @param string $accessToken 164 | * @return void 165 | */ 166 | public function setAccessToken(string $accessToken) 167 | { 168 | $this->pushAuthenticationMiddleware(new AuthenticationMiddleware([ 169 | 'Authorization' => 'Bearer ' . $accessToken 170 | ])); 171 | } 172 | 173 | /** 174 | * Gets the access token. 175 | * 176 | * @return OAuthToken 177 | */ 178 | public function getAccessToken(): OAuthToken 179 | { 180 | return $this->accessToken; 181 | } 182 | 183 | /** 184 | * Gets the refresh token. 185 | * 186 | * @return OAuthToken 187 | */ 188 | public function getRefreshToken(): OAuthToken 189 | { 190 | return $this->refreshToken; 191 | } 192 | 193 | /** 194 | * Creates a UsersFactory to query user information. 195 | * 196 | * @return UsersFactory 197 | */ 198 | public function users(): UsersFactory 199 | { 200 | return new UsersFactory($this->getHttpClient()); 201 | } 202 | 203 | /** 204 | * Gets a trophy title from the API using a communication id (NPWRxxxxx_00). 205 | * 206 | * @param string $npCommunicationId 207 | * @param string $serviceName 208 | * @return TrophyTitle 209 | */ 210 | public function trophies(string $npCommunicationId, string $serviceName = 'trophy'): TrophyTitle 211 | { 212 | return new TrophyTitle($this->getHttpClient(), $npCommunicationId, $serviceName); 213 | } 214 | 215 | /** 216 | * Creates a store factory to navigate the PlayStation Store. 217 | * 218 | * @return StoreFactory 219 | */ 220 | public function store(): StoreFactory 221 | { 222 | return new StoreFactory($this->getHttpClient()); 223 | } 224 | 225 | /** 226 | * Creates a group factory to query your chat groups (parties and text message groups). 227 | * 228 | * @return GroupsFactory 229 | */ 230 | public function groups(): GroupsFactory 231 | { 232 | return new GroupsFactory($this->getHttpClient()); 233 | } 234 | 235 | /** 236 | * Get a media object from the API. 237 | * 238 | * @param string $ugcId 239 | * @return Media 240 | */ 241 | public function media(string $ugcId): Media 242 | { 243 | return new Media($this->getHttpClient(), $ugcId); 244 | } 245 | 246 | /** 247 | * Gets the cloud media gallery for the user. 248 | * 249 | * @return CloudMediaGalleryFactory 250 | */ 251 | public function cloudMediaGallery(): CloudMediaGalleryFactory 252 | { 253 | return new CloudMediaGalleryFactory($this->getHttpClient()); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | 'https://some-redirect.com?code=' . $authCode], '{}'); 27 | $tokenResponse = new Response(200, [], '{"access_token": "some-access-token", "expires_in": 60, "refresh_token": "some-refresh-token", "refresh_token_expires_in": 60}'); 28 | 29 | $this->httpClient 30 | ->expects($this->once()) 31 | ->method('get') 32 | ->with( 33 | Client::AUTH_URL . 'authz/v3/oauth/authorize', 34 | [ 35 | 'query' => $this->getAuthorizeQueryParams(), 36 | 'headers' => [ 37 | 'Cookie' => 'npsso=' . $npSso, 38 | ], 39 | ] 40 | ) 41 | ->willReturn($authorizeResponse->withBody(new JsonStream($authorizeResponse->getBody()))); 42 | 43 | $this->httpClient 44 | ->expects($this->once()) 45 | ->method('post') 46 | ->with( 47 | Client::AUTH_URL . 'authz/v3/oauth/token', 48 | [ 49 | 'form_params' => $this->getTokenFormParams($authCode), 50 | 'headers' => [ 51 | 'Cookie' => 'npsso=' . $npSso, 52 | 'Authorization' => 'Basic YWM4ZDE2MWEtZDk2Ni00NzI4LWIwZWEtZmZlYzIyZjY5ZWRjOkRFaXhFcVhYQ2RYZHdqMHY=', 53 | ], 54 | ] 55 | ) 56 | ->willReturn($tokenResponse->withBody(new JsonStream($tokenResponse->getBody()))); 57 | 58 | $this->httpClient 59 | ->expects($this->atLeastOnce()) 60 | ->method('getConfig') 61 | ->willReturn(['handler' => HandlerStack::create()]); 62 | 63 | $this->client->loginWithNpsso($npSso); 64 | 65 | $this->assertEquals('some-access-token', $this->client->getAccessToken()->getToken()); 66 | $this->assertEquals('some-refresh-token', $this->client->getRefreshToken()->getToken()); 67 | } 68 | 69 | public function testItShouldLoginWithRefreshToken(): void 70 | { 71 | $refreshToken = 'some-refresh-token'; 72 | $response = new Response(200, [], '{"access_token": "some-access-token", "expires_in": 60, "refresh_token": "some-refresh-token", "refresh_token_expires_in": 60}'); 73 | 74 | $this->httpClient 75 | ->expects($this->once()) 76 | ->method('post') 77 | ->with( 78 | 'authz/v3/oauth/token', 79 | [ 80 | 'form_params' => [ 81 | 'scope' => 'psn:mobile.v1 psn:clientapp', 82 | 'refresh_token' => $refreshToken, 83 | 'grant_type' => 'refresh_token', 84 | 'token_format' => 'jwt', 85 | ], 86 | 'headers' => [ 87 | 'Authorization' => 'Basic YWM4ZDE2MWEtZDk2Ni00NzI4LWIwZWEtZmZlYzIyZjY5ZWRjOkRFaXhFcVhYQ2RYZHdqMHY=', 88 | ], 89 | ] 90 | ) 91 | ->willReturn($response->withBody(new JsonStream($response->getBody()))); 92 | 93 | $this->httpClient 94 | ->expects($this->atLeastOnce()) 95 | ->method('getConfig') 96 | ->willReturn(['handler' => HandlerStack::create()]); 97 | 98 | $this->client->loginWithRefreshToken($refreshToken); 99 | } 100 | 101 | public function testItShouldThrowOnInvalidResponseCode(): void 102 | { 103 | $npSso = 'NpSso'; 104 | $authCode = 'AUTH-CODE'; 105 | $authorizeResponse = new Response(404, ['Location' => 'https://some-redirect.com?code=' . $authCode], '{}'); 106 | 107 | $this->httpClient 108 | ->expects($this->once()) 109 | ->method('get') 110 | ->with( 111 | Client::AUTH_URL . 'authz/v3/oauth/authorize', 112 | [ 113 | 'query' => $this->getAuthorizeQueryParams(), 114 | 'headers' => [ 115 | 'Cookie' => 'npsso=' . $npSso, 116 | ], 117 | ] 118 | ) 119 | ->willReturn($authorizeResponse->withBody(new JsonStream($authorizeResponse->getBody()))); 120 | 121 | $this->httpClient 122 | ->expects($this->never()) 123 | ->method('post'); 124 | 125 | $this->httpClient 126 | ->expects($this->never()) 127 | ->method('getConfig'); 128 | 129 | $this->expectException(\Exception::class); 130 | $this->expectExceptionMessage('Incorrect response code from oauth/authorize.'); 131 | $this->client->loginWithNpsso($npSso); 132 | } 133 | 134 | public function testItShouldThrowOnEmptyHeaderLocation(): void 135 | { 136 | $npSso = 'NpSso'; 137 | $authorizeResponse = new Response(302, [], '{}'); 138 | 139 | $this->httpClient 140 | ->expects($this->once()) 141 | ->method('get') 142 | ->with( 143 | Client::AUTH_URL . 'authz/v3/oauth/authorize', 144 | [ 145 | 'query' => $this->getAuthorizeQueryParams(), 146 | 'headers' => [ 147 | 'Cookie' => 'npsso=' . $npSso, 148 | ], 149 | ] 150 | ) 151 | ->willReturn($authorizeResponse->withBody(new JsonStream($authorizeResponse->getBody()))); 152 | 153 | $this->httpClient 154 | ->expects($this->never()) 155 | ->method('post'); 156 | 157 | $this->httpClient 158 | ->expects($this->never()) 159 | ->method('getConfig'); 160 | 161 | $this->expectException(\Exception::class); 162 | $this->expectExceptionMessage('Missing redirect location from oauth/authorize.'); 163 | $this->client->loginWithNpsso($npSso); 164 | } 165 | 166 | public function testItShouldThrowOnEmptyQueryParam(): void 167 | { 168 | $npSso = 'NpSso'; 169 | $authorizeResponse = new Response(302, ['Location' => 'https://some-redirect.com'], '{}'); 170 | 171 | $this->httpClient 172 | ->expects($this->once()) 173 | ->method('get') 174 | ->with( 175 | Client::AUTH_URL . 'authz/v3/oauth/authorize', 176 | [ 177 | 'query' => $this->getAuthorizeQueryParams(), 178 | 'headers' => [ 179 | 'Cookie' => 'npsso=' . $npSso, 180 | ], 181 | ] 182 | ) 183 | ->willReturn($authorizeResponse->withBody(new JsonStream($authorizeResponse->getBody()))); 184 | 185 | $this->httpClient 186 | ->expects($this->never()) 187 | ->method('post'); 188 | 189 | $this->httpClient 190 | ->expects($this->never()) 191 | ->method('getConfig'); 192 | 193 | $this->expectException(\Exception::class); 194 | $this->expectExceptionMessage('Missing code from oauth/authorize.'); 195 | $this->client->loginWithNpsso($npSso); 196 | } 197 | 198 | public function testItShouldReturnFactories(): void 199 | { 200 | $this->assertEquals(new UsersFactory($this->httpClient), $this->client->users()); 201 | $this->assertEquals(new TrophyTitle($this->httpClient, 'id', 'trophy'), $this->client->trophies('id')); 202 | $this->assertEquals(new StoreFactory($this->httpClient), $this->client->store()); 203 | $this->assertEquals(new GroupsFactory($this->httpClient), $this->client->groups()); 204 | $this->assertEquals(new Media($this->httpClient, 'id'), $this->client->media('id')); 205 | $this->assertEquals(new CloudMediaGalleryFactory($this->httpClient), $this->client->cloudMediaGallery()); 206 | } 207 | 208 | protected function setUp(): void 209 | { 210 | parent::setUp(); 211 | 212 | $this->client = new Client(); 213 | $this->httpClient = $this->createMock(\GuzzleHttp\Client::class); 214 | 215 | // Because our Guzzle client is not injected in to the client, 216 | // we need to do some magic to make sure we can mock it. 217 | // Ideally this should be refactored to DI. 218 | $class = new \ReflectionClass(Client::class); 219 | $property = $class->getProperty('httpClient'); 220 | $property->setAccessible(true); 221 | $property->setValue($this->client, $this->httpClient); 222 | } 223 | 224 | private function getAuthorizeQueryParams(): array 225 | { 226 | return [ 227 | 'access_type' => 'offline', 228 | 'app_context' => 'inapp_ios', 229 | 'auth_ver' => 'v3', 230 | 'cid' => '60351282-8C5F-4D5E-9033-E48FEA973E11', 231 | 'client_id' => 'ac8d161a-d966-4728-b0ea-ffec22f69edc', 232 | 'darkmode' => 'true', 233 | 'device_base_font_size' => 10, 234 | 'device_profile' => 'mobile', 235 | 'duid' => '0000000d0004008088347AA0C79542D3B656EBB51CE3EBE1', 236 | 'elements_visibility' => 'no_aclink', 237 | 'extraQueryParams' => '{ 238 | PlatformPrivacyWs1 = minimal; 239 | }', 240 | 'no_captcha' => 'true', 241 | 'redirect_uri' => 'com.playstation.PlayStationApp://redirect', 242 | 'response_type' => 'code', 243 | 'scope' => 'psn:mobile.v1 psn:clientapp', 244 | 'service_entity' => 'urn:service-entity:psn', 245 | 'service_logo' => 'ps', 246 | 'smcid' => 'psapp:settings-entrance', 247 | 'support_scheme' => 'sneiprls', 248 | 'token_format' => 'jwt', 249 | 'ui' => 'pr', 250 | ]; 251 | } 252 | 253 | private function getTokenFormParams(string $authCode): array 254 | { 255 | return [ 256 | 'smcid' => 'psapp%3Asettings-entrance', 257 | 'access_type' => 'offline', 258 | 'code' => $authCode, 259 | 'service_logo' => 'ps', 260 | 'ui' => 'pr', 261 | 'elements_visibility' => 'no_aclink', 262 | 'redirect_uri' => 'com.playstation.PlayStationApp://redirect', 263 | 'support_scheme' => 'sneiprls', 264 | 'grant_type' => 'authorization_code', 265 | 'darkmode' => 'true', 266 | 'device_base_font_size' => 10, 267 | 'device_profile' => 'mobile', 268 | 'app_context' => 'inapp_ios', 269 | 'extraQueryParams' => '{ 270 | PlatformPrivacyWs1 = minimal; 271 | }', 272 | 'token_format' => 'jwt', 273 | ]; 274 | } 275 | } 276 | --------------------------------------------------------------------------------