-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add function to retrieve recently played games (#134)
* feat: add function to fetch recently played games * chore: address linter issues * chore: remove unnecessary hash computation * Update src/user/getUserGameList.test.ts Co-authored-by: Wes Copeland <wlcopeland1@gmail.com> * Update src/user/getUserGameList.test.ts Co-authored-by: Wes Copeland <wlcopeland1@gmail.com> * Update src/user/getUserGameList.ts Co-authored-by: Wes Copeland <wlcopeland1@gmail.com> * chore: resolve merge conflict * chore: separate gpl op hashes into their own file * chore: revert yarn.lock * chore: address yarn verify issues * chore: rename graphql function * chore: rename function files docs: add docs for new recently played games funcs * chore: run lint/fix/verify * chore: move comments related to graphql endpoints to appropriate files chore: refactor getRecentlyPlayedGames test doc: update readme with getRecentlyPlayedGames * chore: address linter issues --------- Co-authored-by: Wes Copeland <wlcopeland1@gmail.com>
- Loading branch information
1 parent
840b131
commit 9846d3a
Showing
12 changed files
with
354 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const GRAPHQL_BASE_URL = | ||
"https://web.np.playstation.com/api/graphql/v1/op"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { rest } from "msw"; | ||
import { setupServer } from "msw/node"; | ||
|
||
import type { | ||
AuthorizationPayload, | ||
RecentlyPlayedGamesResponse | ||
} from "../models"; | ||
import { getRecentlyPlayedGames } from "./getRecentlyPlayedGames"; | ||
import { GRAPHQL_BASE_URL } from "./GRAPHQL_BASE_URL"; | ||
|
||
const server = setupServer(); | ||
const accessToken = "mockAccessToken"; | ||
|
||
describe("Function: getRecentlyPlayedGames", () => { | ||
// MSW Setup | ||
beforeAll(() => | ||
server.listen({ | ||
onUnhandledRequest: "error" | ||
}) | ||
); | ||
afterEach(() => server.resetHandlers()); | ||
afterAll(() => server.close()); | ||
|
||
it("is defined #sanity", () => { | ||
// ASSERT | ||
expect(getRecentlyPlayedGames).toBeDefined(); | ||
}); | ||
|
||
it("retrieves games the user has played recently", async () => { | ||
// ARRANGE | ||
const mockAuthorization: AuthorizationPayload = { | ||
accessToken | ||
}; | ||
|
||
const mockResponse: RecentlyPlayedGamesResponse = { | ||
data: { | ||
gameLibraryTitlesRetrieve: { | ||
__typename: "GameList", | ||
games: [ | ||
{ | ||
__typename: "GameLibraryTitle", | ||
conceptId: "203715", | ||
entitlementId: "EP2002-CUSA01433_00-ROCKETLEAGUEEU01", | ||
image: { | ||
__typename: "Media", | ||
url: "https://image.api.playstation.com/gs2-sec/appkgo/prod/CUSA01433_00/7/i_5c5e430a49994f22df5fd81f446ead7b6ae45027af490b415fe4e744a9918e4c/i/icon0.png" | ||
}, | ||
isActive: true, | ||
lastPlayedDateTime: "2023-03-10T01:01:01.390000Z", | ||
name: "Rocket League®", | ||
platform: "PS4", | ||
productId: "EP2002-CUSA01433_00-ROCKETLEAGUEEU01", | ||
subscriptionService: "NONE", | ||
titleId: "CUSA01433_00" | ||
}, | ||
{ | ||
__typename: "GameLibraryTitle", | ||
conceptId: "10004142", | ||
entitlementId: null, | ||
image: { | ||
__typename: "Media", | ||
url: "https://image.api.playstation.com/vulcan/ap/rnd/202208/2505/DE9sevLlnfHm7vLrRwDFEZpO.png" | ||
}, | ||
isActive: null, | ||
lastPlayedDateTime: "2023-01-19T01:01:01.900000Z", | ||
name: "CRISIS CORE –FINAL FANTASY VII– REUNION PS4 & PS5", | ||
platform: "PS5", | ||
productId: null, | ||
subscriptionService: "NONE", | ||
titleId: "PPSA07809_00" | ||
} | ||
] | ||
} | ||
} | ||
}; | ||
|
||
let headers!: Record<string, string>; | ||
let searchParams!: URLSearchParams; | ||
|
||
server.use( | ||
rest.get(GRAPHQL_BASE_URL, (_, res, ctx) => { | ||
headers = _.headers.raw(); | ||
searchParams = _.url.searchParams; | ||
|
||
return res(ctx.json(mockResponse)); | ||
}) | ||
); | ||
|
||
// ACT | ||
const response = await getRecentlyPlayedGames(mockAuthorization, { | ||
categories: ["ps4_game", "ps5_native_game"], | ||
limit: 2 | ||
}); | ||
|
||
// ASSERT | ||
expect(response).toEqual(mockResponse); | ||
expect(headers["authorization"]).toEqual(`Bearer ${accessToken}`); | ||
expect(searchParams.get("operationName")).toEqual("getUserGameList"); | ||
expect(searchParams.get("variables")).toEqual( | ||
JSON.stringify({ limit: 2, categories: "ps4_game,ps5_native_game" }) | ||
); | ||
expect(searchParams.get("extensions")).toEqual( | ||
JSON.stringify({ | ||
persistedQuery: { | ||
version: 1, | ||
sha256Hash: | ||
"e780a6d8b921ef0c59ec01ea5c5255671272ca0d819edb61320914cf7a78b3ae" | ||
} | ||
}) | ||
); | ||
}); | ||
|
||
it("throws an error if we receive a malformed response", async () => { | ||
// ARRANGE | ||
const mockAuthorization: AuthorizationPayload = { | ||
accessToken | ||
}; | ||
|
||
const mockResponse = { | ||
// This response occurs if the query/hash is not what the server expected | ||
message: | ||
"Query 4e8add9915e3cb6870d778cff38e7f81899066f5603ced4c87d6d7c0abc99941 not whitelisted" | ||
}; | ||
|
||
server.use( | ||
rest.get(GRAPHQL_BASE_URL, (_, res, ctx) => { | ||
return res(ctx.json(mockResponse)); | ||
}) | ||
); | ||
|
||
// ASSERT | ||
await expect(getRecentlyPlayedGames(mockAuthorization)).rejects.toThrow( | ||
"Query 4e8add9915e3cb6870d778cff38e7f81899066f5603ced4c87d6d7c0abc99941 not whitelisted" | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { | ||
AllCallOptions, | ||
AuthorizationPayload, | ||
RecentlyPlayedGamesResponse | ||
} from "../models"; | ||
import { call } from "../utils/call"; | ||
import { GRAPHQL_BASE_URL } from "./GRAPHQL_BASE_URL"; | ||
import { getUserGameListHash } from "./operationHashes"; | ||
|
||
type GetRecentlyPlayedGamesOptionsCategories = "ps4_game" | "ps5_native_game"; | ||
type GetRecentlyPlayedGamesOptions = Pick<AllCallOptions, "limit"> & { | ||
categories: GetRecentlyPlayedGamesOptionsCategories[]; | ||
}; | ||
|
||
/** | ||
* A call to this function will retrieve recently played games for the user associated | ||
* with the npsso token provided to this module during initialisation. | ||
* | ||
* This is useful if you want recent activity that isn't tied to trophy progress. | ||
* | ||
* @param authorization An object containing your access token, typically retrieved with `exchangeCodeForAccessToken()`. | ||
*/ | ||
export const getRecentlyPlayedGames = async ( | ||
authorization: AuthorizationPayload, | ||
options: Partial<GetRecentlyPlayedGamesOptions> = {} | ||
): Promise<RecentlyPlayedGamesResponse> => { | ||
const { limit = 50, categories = ["ps4_game", "ps5_native_game"] } = options; | ||
|
||
const url = new URL(GRAPHQL_BASE_URL); | ||
|
||
url.searchParams.set("operationName", "getUserGameList"); | ||
url.searchParams.set( | ||
"variables", | ||
JSON.stringify({ | ||
limit, | ||
categories: categories.join(",") | ||
}) | ||
); | ||
url.searchParams.set( | ||
"extensions", | ||
JSON.stringify({ | ||
persistedQuery: { | ||
version: 1, | ||
sha256Hash: getUserGameListHash | ||
} | ||
}) | ||
); | ||
|
||
const response = await call<RecentlyPlayedGamesResponse>( | ||
{ url: url.toString() }, | ||
authorization | ||
); | ||
|
||
// The GraphQL queries can return non-truthy values. | ||
if (!response.data || !response.data.gameLibraryTitlesRetrieve) { | ||
throw new Error(JSON.stringify(response)); | ||
} | ||
|
||
return response; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./getRecentlyPlayedGames"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* GraphQL endpoints work differently to others in the codebase. | ||
* | ||
* The hashes in this file are reverse engineered from app-<hash>.js file loaded by the page | ||
* at https://library.playstation.com/recently-played. Following the code in that file leads | ||
* to some Apollo GraphQL code related to persisted queries. This means the request needs to | ||
* contain a SHA256 hash of the GraphQL query being executed. Searching for PersistedQueryLink | ||
* and createPersistedQueryLink_hashes, and an AST function in Sony's JS source and debugging | ||
* will surface the exact GraphQL query that's passed to the hash function on the page. | ||
* | ||
* Thankfully it's easier to figure out future endpoints and hashes by: | ||
* | ||
* 1. Visiting a page, e.g https://library.playstation.com/recently-played | ||
* 2. Using DevTools to find requests to https://web.np.playstation.com/api/graphql/v1/op | ||
* 3. Decoding the URL parameters to find the correct SHA256 hash and some of the supported parameters | ||
*/ | ||
|
||
// Hash is computed from the following query (without surrounding quotes): | ||
// "query getUserGameList($categories: String, $limit: Int, $orderBy: String, $subscriptionService: SubscriptionService) {\n gameLibraryTitlesRetrieve(categories: $categories, limit: $limit, orderBy: $orderBy, subscriptionService: $subscriptionService) {\n __typename\n games {\n __typename\n conceptId\n entitlementId\n image {\n __typename\n url\n }\n isActive\n lastPlayedDateTime\n name\n platform\n productId\n subscriptionService\n titleId\n }\n }\n}\n" | ||
export const getUserGameListHash = | ||
"e780a6d8b921ef0c59ec01ea5c5255671272ca0d819edb61320914cf7a78b3ae"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { TitlePlatform } from "./title-platform.model"; | ||
|
||
export interface RecentlyPlayedGame { | ||
/** GrahpQL object type/schema */ | ||
__typename: "GameLibraryTitle"; | ||
|
||
/** Contains a url to a game icon file. */ | ||
image: { | ||
__typename: "Media"; | ||
url: string; | ||
}; | ||
|
||
/** | ||
* Unclear what this represents, but if this is set to true then | ||
* the productId property is not null. | ||
*/ | ||
isActive: boolean | null; | ||
|
||
/** | ||
* An ISO date representing the last play date and time | ||
* of the given title, e.g 2023-03-10T01:01:01.390000Z | ||
*/ | ||
lastPlayedDateTime: string; | ||
|
||
/** The name of the game */ | ||
name: string; | ||
|
||
/** | ||
* The platform this game was last played on. This can be reported as | ||
* "UNKNOWN". It appears that "UNKNOWN" is shown in certain scenarios | ||
* when a user that isn't associated with the access token is sharing | ||
* the same console as the user that is identified by the access token. | ||
*/ | ||
platform: TitlePlatform | "UNKNOWN"; | ||
|
||
/** | ||
* ID of the product. Used in the PlayStation Store URL. Appears to only be | ||
* set for certain PS4 games along with the entitlementId. | ||
*/ | ||
productId: string | null; | ||
|
||
/** | ||
* Similar to productId. Used in URLs for the PlayStation Store, i.e | ||
* store.playstation.com/en-ae/product/:entitlementId. Appears to only | ||
* be set for certain entries on the PS4 platform along with productId. | ||
*/ | ||
entitlementId: string | null; | ||
|
||
/** | ||
* ID of the product. Forms part of a PlayStation Store URL, e.g | ||
* https://store.playstation.com/en-us/product/UP9000-$titleId-RATCHETCLANKRIFT | ||
*/ | ||
titleId: string; | ||
|
||
/** | ||
* An ID for titles on the PlayStation store. It's used in the | ||
* URL, i.e store.playstation.com/ko-kr/concept/:conceptId | ||
*/ | ||
conceptId: string; | ||
|
||
/** Unsure what data this can hold. Perhaps it's PS Now related? */ | ||
subscriptionService: "NONE" | string; | ||
} | ||
|
||
export interface RecentlyPlayedGamesResponse { | ||
data: { | ||
gameLibraryTitlesRetrieve: { | ||
__typename: "GameList"; | ||
games: RecentlyPlayedGame[]; | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# Recently Played Game | ||
|
||
| Name | Type | Description | | ||
| :------------------- | :--------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `name` | `string` | The name of the game. | | ||
| `lastPlayedDateTime` | `string` | An ISO date representing the last play date and time of the given title, e.g 2023-03-10T01:01:01.390000Z. | | ||
| `conceptId` | `string` | An ID for titles on the PlayStation store. It's used in the URL, like so `store.playstation.com/en-is/concept/:conceptId`. | | ||
| `titleId` | `string` | ID of the product. Forms part of a PlayStation Store URL, e.g `https://store.playstation.com/en-us/product/UP9000-$titleId-RATCHETCLANKRIFT` | | ||
| `platform` | `TitlePlatform`\|`"UNKNOWN"` | The platform this game was last played on. This can be reported as "UNKNOWN". It appears that "UNKNOWN" is shown in certain scenarios, such as when a user that **isn't** associated with the access token is sharing the same console as the user that **is** identified by the access token. | | ||
| `entitlementId` | `string`\|`null` | Similar to productId. Used in URLs for the PlayStation Store, like so `store.playstation.com/en-ae/product/:entitlementId`. Appears to only be set for certain entries on the PS4 platform along with `productId`. | | ||
| `productId` | `string`\|`null` | ID of the product. Used in the PlayStation Store URL. Appears to only be set for certain PS4 games along with the entitlementId. | | ||
| `image.url` | `string` | Contains a URL to a game icon file. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9846d3a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
psn-api – ./
psn-api-git-main-achievements-app.vercel.app
psn-api-achievements-app.vercel.app
psn-api.achievements.app