├── .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 | [](https://github.com/Tustin/psn-php/stargazers)
6 | [](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 |
--------------------------------------------------------------------------------