From d50fcfae0375345a797f9626a72af06a411f409d Mon Sep 17 00:00:00 2001 From: Joshua Raphael Date: Wed, 25 Sep 2024 07:29:29 -0700 Subject: [PATCH] add GetUserProgress endpoint --- README.md | 1 + .../user/getuserprogress/getuserprogress.go | 25 +++ game.go | 4 +- http/request.go | 11 +- http/request_test.go | 4 +- models/progress.go | 10 ++ user.go | 19 +++ user_test.go | 161 ++++++++++++++++++ 8 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 examples/user/getuserprogress/getuserprogress.go create mode 100644 models/progress.go diff --git a/README.md b/README.md index 6a74e37..12856d3 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ For convenience, the API docs and examples can be found in the tables below |`GetUserClaims(string)`|Get a list of set development claims made over the lifetime of a user.|[docs](https://api-docs.retroachievements.org/v1/get-user-claims.html) \| [example](examples/user/getuserclaims/getuserclaims.go)| |`GetUserGameRankAndScore(string,int)`|Get metadata about how a user has performed on a given game.|[docs](https://api-docs.retroachievements.org/v1/get-user-game-rank-and-score.html) \| [example](examples/user/getusergamerankandscore/getusergamerankandscore.go)| |`GetUserPoints(string)`|Get a user's total hardcore and softcore points.|[docs](https://api-docs.retroachievements.org/v1/get-user-points.html) \| [example](examples/user/getuserpoints/getuserpoints.go)| +|`GetUserProgress(string,[]int)`|Get a user's progress on a list of specified games.|[docs](https://api-docs.retroachievements.org/v1/get-user-progress.html) \| [example](examples/user/getuserprogress/getuserprogress.go)|

Game

diff --git a/examples/user/getuserprogress/getuserprogress.go b/examples/user/getuserprogress/getuserprogress.go new file mode 100644 index 0000000..0c5cec8 --- /dev/null +++ b/examples/user/getuserprogress/getuserprogress.go @@ -0,0 +1,25 @@ +// Package getuserprogress provides an example for getting a users game progress +package main + +import ( + "fmt" + "os" + + "github.com/joshraphael/go-retroachievements" +) + +/* +Test script, add RA_API_KEY to your env and use `go run getuserprogress.go` +*/ +func main() { + secret := os.Getenv("RA_API_KEY") + + client := retroachievements.NewClient(secret) + + resp, err := client.GetUserProgress("jamiras", []int{1, 16247}) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", resp) +} diff --git a/game.go b/game.go index a9c075f..599c0d4 100644 --- a/game.go +++ b/game.go @@ -14,7 +14,7 @@ func (c *Client) GetGame(id int) (*models.GameInfo, error) { raHttp.Method(http.MethodGet), raHttp.Path("/API/API_GetGame.php"), raHttp.APIToken(c.Secret), - raHttp.ID(id), + raHttp.IDs([]int{id}), ) if err != nil { return nil, fmt.Errorf("calling endpoint: %w", err) @@ -32,7 +32,7 @@ func (c *Client) GetGameExtended(id int) (*models.ExtentedGameInfo, error) { raHttp.Method(http.MethodGet), raHttp.Path("/API/API_GetGameExtended.php"), raHttp.APIToken(c.Secret), - raHttp.ID(id), + raHttp.IDs([]int{id}), ) if err != nil { return nil, fmt.Errorf("calling endpoint: %w", err) diff --git a/http/request.go b/http/request.go index dfec447..23178f8 100644 --- a/http/request.go +++ b/http/request.go @@ -3,6 +3,7 @@ package http import ( "fmt" "strconv" + "strings" "time" ) @@ -87,10 +88,14 @@ func Date(t time.Time) RequestDetail { }) } -// ID adds a target game id to the query parameters -func ID(id int) RequestDetail { +// IDs adds the target game ids to the query parameters +func IDs(ids []int) RequestDetail { + var strIDs []string + for _, i := range ids { + strIDs = append(strIDs, strconv.Itoa(i)) + } return requestDetailFn(func(r *Request) { - r.Params["i"] = strconv.Itoa(id) + r.Params["i"] = strings.Join(strIDs, ",") }) } diff --git a/http/request_test.go b/http/request_test.go index 6adf895..7634dd3 100644 --- a/http/request_test.go +++ b/http/request_test.go @@ -24,7 +24,7 @@ func TestNewRequest(t *testing.T) { raHttp.FromTime(now), raHttp.ToTime(later), raHttp.Date(now), - raHttp.ID(2837), + raHttp.IDs([]int{2837, 4535}), raHttp.GameID(345), raHttp.AwardMetadata(true), ) @@ -43,7 +43,7 @@ func TestNewRequest(t *testing.T) { "f": "1709400423", "t": "1709401023", "d": "2024-03-02", - "i": "2837", + "i": "2837,4535", "g": "345", "a": "1", }, diff --git a/models/progress.go b/models/progress.go new file mode 100644 index 0000000..9e81d91 --- /dev/null +++ b/models/progress.go @@ -0,0 +1,10 @@ +package models + +type Progress struct { + NumPossibleAchievements int `json:"NumPossibleAchievements"` + PossibleScore int `json:"PossibleScore"` + NumAchieved int `json:"NumAchieved"` + ScoreAchieved int `json:"ScoreAchieved"` + NumAchievedHardcore int `json:"NumAchievedHardcore"` + ScoreAchievedHardcore int `json:"ScoreAchievedHardcore"` +} diff --git a/user.go b/user.go index 066c757..27afa0d 100644 --- a/user.go +++ b/user.go @@ -195,3 +195,22 @@ func (c *Client) GetUserPoints(username string) (*models.Points, error) { } return points, nil } + +// GetUserProgress get a user's progress on a list of specified games. +func (c *Client) GetUserProgress(username string, gameIDs []int) (map[string]models.Progress, error) { + resp, err := c.do( + raHttp.Method(http.MethodGet), + raHttp.Path("/API/API_GetUserProgress.php"), + raHttp.APIToken(c.Secret), + raHttp.Username(username), + raHttp.IDs(gameIDs), + ) + if err != nil { + return nil, fmt.Errorf("calling endpoint: %w", err) + } + progress, err := raHttp.ResponseObject[map[string]models.Progress](resp) + if err != nil { + return nil, fmt.Errorf("parsing response object: %w", err) + } + return *progress, nil +} diff --git a/user_test.go b/user_test.go index ac87241..94518cf 100644 --- a/user_test.go +++ b/user_test.go @@ -1513,3 +1513,164 @@ func TestGetUserPoints(tt *testing.T) { }) } } + +func TestGetUserProgress(tt *testing.T) { + tests := []struct { + name string + username string + gameIDs []int + modifyURL func(url string) string + responseCode int + responseMessage map[string]models.Progress + responseError models.ErrorResponse + response func(messageBytes []byte, errorBytes []byte) []byte + assert func(t *testing.T, progress map[string]models.Progress, err error) + }{ + { + name: "fail to call endpoint", + username: "Test", + gameIDs: []int{1, 2, 5352}, + modifyURL: func(url string) string { + return "" + }, + responseCode: http.StatusUnauthorized, + responseError: models.ErrorResponse{ + Message: "test", + Errors: []models.ErrorDetail{ + { + Status: http.StatusUnauthorized, + Code: "unauthorized", + Title: "Not Authorized", + }, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return errorBytes + }, + assert: func(t *testing.T, progress map[string]models.Progress, err error) { + require.Nil(t, progress) + require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetUserProgress.php?i=1%2C2%2C5352&u=Test&y=some_secret\": unsupported protocol scheme \"\"") + }, + }, + { + name: "error response", + username: "Test", + gameIDs: []int{1, 2, 5352}, + modifyURL: func(url string) string { + return url + }, + responseCode: http.StatusUnauthorized, + responseError: models.ErrorResponse{ + Message: "test", + Errors: []models.ErrorDetail{ + { + Status: http.StatusUnauthorized, + Code: "unauthorized", + Title: "Not Authorized", + }, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return errorBytes + }, + assert: func(t *testing.T, progress map[string]models.Progress, err error) { + require.Nil(t, progress) + require.EqualError(t, err, "parsing response object: error responses: [401] Not Authorized") + }, + }, + { + name: "success", + username: "Test", + gameIDs: []int{1, 2, 5352}, + modifyURL: func(url string) string { + return url + }, + responseCode: http.StatusOK, + responseMessage: map[string]models.Progress{ + "1": { + NumPossibleAchievements: 36, + PossibleScore: 305, + NumAchieved: 13, + ScoreAchieved: 100, + NumAchievedHardcore: 13, + ScoreAchievedHardcore: 100, + }, + "2": { + NumPossibleAchievements: 56, + PossibleScore: 600, + NumAchieved: 0, + ScoreAchieved: 0, + NumAchievedHardcore: 0, + ScoreAchievedHardcore: 0, + }, + "5352": { + NumPossibleAchievements: 13, + PossibleScore: 230, + NumAchieved: 10, + ScoreAchieved: 200, + NumAchievedHardcore: 10, + ScoreAchievedHardcore: 200, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return messageBytes + }, + assert: func(t *testing.T, progress map[string]models.Progress, err error) { + require.NotNil(t, progress) + // first element + first, ok := progress["1"] + require.True(t, ok) + require.Equal(t, 36, first.NumPossibleAchievements) + require.Equal(t, 305, first.PossibleScore) + require.Equal(t, 13, first.NumAchieved) + require.Equal(t, 100, first.ScoreAchieved) + require.Equal(t, 13, first.NumAchievedHardcore) + require.Equal(t, 100, first.ScoreAchievedHardcore) + + // second element + second, ok := progress["2"] + require.True(t, ok) + require.Equal(t, 56, second.NumPossibleAchievements) + require.Equal(t, 600, second.PossibleScore) + require.Equal(t, 0, second.NumAchieved) + require.Equal(t, 0, second.ScoreAchieved) + require.Equal(t, 0, second.NumAchievedHardcore) + require.Equal(t, 0, second.ScoreAchievedHardcore) + + // third element + third, ok := progress["5352"] + require.True(t, ok) + require.Equal(t, 13, third.NumPossibleAchievements) + require.Equal(t, 230, third.PossibleScore) + require.Equal(t, 10, third.NumAchieved) + require.Equal(t, 200, third.ScoreAchieved) + require.Equal(t, 10, third.NumAchievedHardcore) + require.Equal(t, 200, third.ScoreAchievedHardcore) + require.NoError(t, err) + }, + }, + } + for _, test := range tests { + tt.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/API/API_GetUserProgress.php" + if r.URL.Path != expectedPath { + t.Errorf("Expected to request '%s', got: %s", expectedPath, r.URL.Path) + } + w.WriteHeader(test.responseCode) + responseMessage, err := json.Marshal(test.responseMessage) + require.NoError(t, err) + errBytes, err := json.Marshal(test.responseError) + require.NoError(t, err) + resp := test.response(responseMessage, errBytes) + num, err := w.Write(resp) + require.NoError(t, err) + require.Equal(t, num, len(resp)) + })) + defer server.Close() + client := retroachievements.New(test.modifyURL(server.URL), "some_secret") + progress, err := client.GetUserProgress(test.username, test.gameIDs) + test.assert(t, progress, err) + }) + } +}