Skip to content

Commit

Permalink
add GetUserProgress endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
joshraphael committed Sep 25, 2024
1 parent a126d36 commit d50fcfa
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)|

<h3>Game</h3>

Expand Down
25 changes: 25 additions & 0 deletions examples/user/getuserprogress/getuserprogress.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions http/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http
import (
"fmt"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -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, ",")
})
}

Expand Down
4 changes: 2 additions & 2 deletions http/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand All @@ -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",
},
Expand Down
10 changes: 10 additions & 0 deletions models/progress.go
Original file line number Diff line number Diff line change
@@ -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"`
}
19 changes: 19 additions & 0 deletions user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
161 changes: 161 additions & 0 deletions user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}

0 comments on commit d50fcfa

Please sign in to comment.