diff --git a/README.md b/README.md index c3fbe2e..206ff62 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,10 @@ Click the function names to open their complete docs on the docs site. - [`getRecentGameAwards()`](https://api-docs.retroachievements.org/v1/get-recent-game-awards.html) - Get all recent mastery, completion, and beaten awards earned on the site. - [`getTopTenUsers()`](https://api-docs.retroachievements.org/v1/get-top-ten-users.html) - Get the list of top ten points earners. +### Comment + +- [`getComments()`](https://api-docs.retroachievements.org/v1/get-comments.html) - Get the comments left an achievement, game, or user wall. + ### Event - [`getAchievementOfTheWeek()`](https://api-docs.retroachievements.org/v1/get-achievement-of-the-week.html) - Get comprehensive metadata about the current Achievement of the Week. diff --git a/src/comment/getComments.test.ts b/src/comment/getComments.test.ts new file mode 100644 index 0000000..03a361b --- /dev/null +++ b/src/comment/getComments.test.ts @@ -0,0 +1,277 @@ +/* eslint-disable sonarjs/no-duplicate-string */ + +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; + +import { apiBaseUrl } from "../utils/internal"; +import { buildAuthorization } from "../utils/public"; +import { getComments } from "./getComments"; +import type { CommentsResponse, GetCommentsResponse } from "./models"; + +const server = setupServer(); + +describe("Function: getComments", () => { + // MSW Setup + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + it("is defined #sanity", () => { + // ASSERT + expect(getComments).toBeDefined(); + }); + + it("retrieves the comments on a user's wall", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse: GetCommentsResponse = { + Count: 2, + Total: 2, + Results: [ + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 1", + }, + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 2", + }, + ], + }; + + server.use( + http.get(`${apiBaseUrl}/API_GetComments.php`, () => + HttpResponse.json(mockResponse) + ) + ); + + // ACT + const response = await getComments(authorization, { + identifier: "xelnia", + }); + + // ASSERT + const expectedResponse: CommentsResponse = { + count: 2, + total: 2, + results: [ + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 1", + }, + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 2", + }, + ], + }; + + expect(response).toEqual(expectedResponse); + }); + + it("retrieves the comments on an game", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse: GetCommentsResponse = { + Count: 2, + Total: 2, + Results: [ + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 1", + }, + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 2", + }, + ], + }; + + server.use( + http.get(`${apiBaseUrl}/API_GetComments.php`, () => + HttpResponse.json(mockResponse) + ) + ); + + // ACT + const response = await getComments(authorization, { + identifier: 321_865, + kind: "game", + }); + + // ASSERT + const expectedResponse: CommentsResponse = { + count: 2, + total: 2, + results: [ + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 1", + }, + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 2", + }, + ], + }; + + expect(response).toEqual(expectedResponse); + }); + + it("retrieves the comments on an achievement, respecting count", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse: GetCommentsResponse = { + Count: 2, + Total: 4, + Results: [ + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 1", + }, + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 2", + }, + ], + }; + + server.use( + http.get(`${apiBaseUrl}/API_GetComments.php`, () => + HttpResponse.json(mockResponse) + ) + ); + + // ACT + const response = await getComments(authorization, { + identifier: 321_865, + count: 2, + kind: "achievement", + }); + + // ASSERT + const expectedResponse: CommentsResponse = { + count: 2, + total: 4, + results: [ + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 1", + }, + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 2", + }, + ], + }; + + expect(response).toEqual(expectedResponse); + }); + + it("retrieves the comments on an game, respecting offset", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse: GetCommentsResponse = { + Count: 1, + Total: 2, + Results: [ + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 2", + }, + ], + }; + + server.use( + http.get(`${apiBaseUrl}/API_GetComments.php`, () => + HttpResponse.json(mockResponse) + ) + ); + + // ACT + const response = await getComments(authorization, { + identifier: 321_865, + offset: 1, + kind: "game", + }); + + // ASSERT + const expectedResponse: CommentsResponse = { + count: 1, + total: 2, + results: [ + { + user: "PlayTester", + submitted: "2024-07-31T11:22:23.000000Z", + commentText: "Comment 2", + }, + ], + }; + + expect(response).toEqual(expectedResponse); + }); + + it("warns the developer when they don't specify kind for achievements/games", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse: GetCommentsResponse = { + Count: 1, + Total: 2, + Results: [ + { + User: "PlayTester", + Submitted: "2024-07-31T11:22:23.000000Z", + CommentText: "Comment 2", + }, + ], + }; + + server.use( + http.get(`${apiBaseUrl}/API_GetComments.php`, () => + HttpResponse.json(mockResponse) + ) + ); + + // ACT + const response = getComments(authorization, { + identifier: 321_865, + offset: 1, + }); + + // ASSERT + await expect(response).rejects.toThrow(TypeError); + }); +}); diff --git a/src/comment/getComments.ts b/src/comment/getComments.ts new file mode 100644 index 0000000..a8a3522 --- /dev/null +++ b/src/comment/getComments.ts @@ -0,0 +1,107 @@ +import type { ID } from "../utils/internal"; +import { + apiBaseUrl, + buildRequestUrl, + call, + serializeProperties, +} from "../utils/internal"; +import type { AuthObject } from "../utils/public"; +import type { CommentsResponse, GetCommentsResponse } from "./models"; + +const kindMap: Record<"game" | "achievement" | "user", number> = { + game: 1, + achievement: 2, + user: 3, +}; + +/** + * A call to this function will retrieve a list of comments on a particular target. + * + * @param authorization An object containing your username and webApiKey. + * This can be constructed with `buildAuthorization()`. + * + * @param payload.identifier The identifier to retrieve. For user walls, this will + * be a string (the username), and for game and achievement walls, this will be a + * the ID of the object in question. + * + * @param payload.kind What kind of identifier was used. This can be "game", + * "achievement", or "user". + * + * @param payload.offset Defaults to 0. The number of entries to skip. + * + * @param payload.count Defaults to 50, has a max of 500. + * + * @example + * ``` + * // Retrieving game/achievement comments + * const gameWallComments = await getComments( + * authorization, + * { identifier: 20294, kind: 'game', count: 4, offset: 0 }, + * ); + * + * // Retrieving comments on a user's wall + * const userWallComments = await getComments( + * authorization, + * { identifier: "xelnia", count: 4, offset: 0 }, + * ); + * ``` + * + * @returns An object containing the amount of comments retrieved, + * the total comments, and an array of the comments themselves. + * ``` + * { + * count: 4, + * total: 4, + * results: [ + * { + * user: "PlayTester", + * submitted: "2024-07-31T11:22:23.000000Z", + * commentText: "Comment 1" + * }, + * // ... + * ] + * } + * ``` + */ +export const getComments = async ( + authorization: AuthObject, + payload: { + identifier: ID; + kind?: "game" | "achievement" | "user"; + offset?: number; + count?: number; + } +): Promise => { + const { identifier, kind, offset, count } = payload; + + const queryParams: Record = { i: identifier }; + + if (kind) { + queryParams.t = kindMap[kind]; + } else if (typeof identifier === "number") { + throw new TypeError( + "'kind' must be specified when looking up an achievement or game." + ); + } + + if (offset) { + queryParams.o = offset; + } + + if (count) { + queryParams.c = count; + } + + const url = buildRequestUrl( + apiBaseUrl, + "/API_GetComments.php", + authorization, + queryParams + ); + + const rawResponse = await call({ url }); + + return serializeProperties(rawResponse, { + shouldCastToNumbers: ["Count", "Total"], + }); +}; diff --git a/src/comment/index.ts b/src/comment/index.ts new file mode 100644 index 0000000..397b938 --- /dev/null +++ b/src/comment/index.ts @@ -0,0 +1,2 @@ +export * from "./getComments"; +export * from "./models"; diff --git a/src/comment/models/comment-entity.model.ts b/src/comment/models/comment-entity.model.ts new file mode 100644 index 0000000..f203b46 --- /dev/null +++ b/src/comment/models/comment-entity.model.ts @@ -0,0 +1,7 @@ +export interface CommentEntity { + user: string; + submitted: string; + commentText: string; +} + +export type Comment = CommentEntity; diff --git a/src/comment/models/comment-response.model.ts b/src/comment/models/comment-response.model.ts new file mode 100644 index 0000000..2f65cf0 --- /dev/null +++ b/src/comment/models/comment-response.model.ts @@ -0,0 +1,7 @@ +import type { CommentEntity } from "./comment-entity.model"; + +export interface CommentsResponse { + count: number; + total: number; + results: CommentEntity[]; +} diff --git a/src/comment/models/get-comments-response.model.ts b/src/comment/models/get-comments-response.model.ts new file mode 100644 index 0000000..1aeb03b --- /dev/null +++ b/src/comment/models/get-comments-response.model.ts @@ -0,0 +1,13 @@ +interface RawCommentsResponseEntity { + Count: number; + Total: number; + Results: RawComment[]; +} + +interface RawComment { + User: string; + Submitted: string; + CommentText: string; +} + +export type GetCommentsResponse = RawCommentsResponseEntity; diff --git a/src/comment/models/index.ts b/src/comment/models/index.ts new file mode 100644 index 0000000..3d9301c --- /dev/null +++ b/src/comment/models/index.ts @@ -0,0 +1,3 @@ +export * from "./comment-entity.model"; +export * from "./comment-response.model"; +export * from "./get-comments-response.model"; diff --git a/src/index.ts b/src/index.ts index 15334be..82bbcfb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ // This file is the single public-facing API of the entire library. export * from "./achievement"; +export * from "./comment"; export * from "./console"; export * from "./feed"; export * from "./game";