From 560f3cc081b772dbe5365722836ef548441afffa Mon Sep 17 00:00:00 2001 From: Joshua Raphael Date: Sun, 24 Nov 2024 13:11:14 -0700 Subject: [PATCH] add GetGameTicketStats endpoint --- README.md | 3 +- .../getgameticketstats/getgameticketstats.go | 30 +++ feed.go | 4 +- http/request.go | 7 +- http/request_test.go | 2 +- models/ticket.go | 45 +++++ ticket.go | 25 +++ ticket_test.go | 184 ++++++++++++++++++ user.go | 3 +- 9 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 examples/ticket/getgameticketstats/getgameticketstats.go diff --git a/README.md b/README.md index 039f484..c7c9fbe 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,5 @@ For convenience, the API docs and examples can be found in the tables below |-|-|-| |`GetTicketByID()`|Gets ticket metadata information about a single achievement ticket, targeted by its ticket ID.|[docs](https://api-docs.retroachievements.org/v1/get-ticket-data/get-ticket-by-id.html) \| [example](examples/ticket/getticketbyid/getticketbyid.go)| |`GetMostTicketedGames()`|Gets the games on the site with the highest count of opened achievement tickets.|[docs](https://api-docs.retroachievements.org/v1/get-ticket-data/get-most-ticketed-games.html) \| [example](examples/ticket/getmostticketedgames/getmostticketedgames.go)| -|`GetMostRecentTickets()`|Gets ticket metadata information about the latest opened achievement tickets on RetroAchievements.|[docs](https://api-docs.retroachievements.org/v1/get-ticket-data/get-most-recent-tickets.html) \| [example](examples/ticket/getmostrecenttickets/getmostrecenttickets.go)| \ No newline at end of file +|`GetMostRecentTickets()`|Gets ticket metadata information about the latest opened achievement tickets on RetroAchievements.|[docs](https://api-docs.retroachievements.org/v1/get-ticket-data/get-most-recent-tickets.html) \| [example](examples/ticket/getmostrecenttickets/getmostrecenttickets.go)| +|`GetGameTicketStats()`|Gets ticket stats for a game, targeted by that game's unique ID.|[docs](https://api-docs.retroachievements.org/v1/get-ticket-data/get-game-ticket-stats.html) \| [example](examples/ticket/getgameticketstats/getgameticketstats.go)| \ No newline at end of file diff --git a/examples/ticket/getgameticketstats/getgameticketstats.go b/examples/ticket/getgameticketstats/getgameticketstats.go new file mode 100644 index 0000000..82f4378 --- /dev/null +++ b/examples/ticket/getgameticketstats/getgameticketstats.go @@ -0,0 +1,30 @@ +// Package getgameticketstats provides an example for getting ticket stats for a game, targeted by that game's unique ID. +package main + +import ( + "fmt" + "os" + + "github.com/joshraphael/go-retroachievements" + "github.com/joshraphael/go-retroachievements/models" +) + +/* +Test script, add RA_API_KEY to your env and use `go run getgameticketstats.go` +*/ +func main() { + secret := os.Getenv("RA_API_KEY") + + client := retroachievements.NewClient(secret) + + metadata := true + resp, err := client.GetGameTicketStats(models.GetGameTicketStatsParameters{ + GameID: 1, + IncludeTicketMetadata: &metadata, + }) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", resp) +} diff --git a/feed.go b/feed.go index 7a9ae11..80f8913 100644 --- a/feed.go +++ b/feed.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "time" raHttp "github.com/joshraphael/go-retroachievements/http" "github.com/joshraphael/go-retroachievements/models" @@ -17,7 +18,8 @@ func (c *Client) GetRecentGameAwards(params models.GetRecentGameAwardsParameters raHttp.APIToken(c.Secret), } if params.StartingDate != nil { - details = append(details, raHttp.D(*params.StartingDate)) + d := *params.StartingDate + details = append(details, raHttp.D(d.UTC().Format(time.DateOnly))) } if params.Count != nil { details = append(details, raHttp.C(*params.Count)) diff --git a/http/request.go b/http/request.go index 7978da3..c3679f6 100644 --- a/http/request.go +++ b/http/request.go @@ -4,7 +4,6 @@ import ( "fmt" "strconv" "strings" - "time" ) // Request holds values for an http call @@ -88,10 +87,10 @@ func T(t int) RequestDetail { }) } -// D adds a 'd' date to the query parameters -func D(d time.Time) RequestDetail { +// D adds a 'd' string to the query parameters +func D(d string) RequestDetail { return requestDetailFn(func(r *Request) { - r.Params["d"] = d.UTC().Format(time.DateOnly) + r.Params["d"] = d }) } diff --git a/http/request_test.go b/http/request_test.go index 5a2e9a2..fc2d417 100644 --- a/http/request_test.go +++ b/http/request_test.go @@ -25,7 +25,7 @@ func TestNewRequest(t *testing.T) { raHttp.M(10), raHttp.F(int(now.Unix())), raHttp.T(int(later.Unix())), - raHttp.D(now), + raHttp.D(now.UTC().Format(time.DateOnly)), raHttp.I([]string{strconv.Itoa(2837), strconv.Itoa(4535)}), raHttp.K([]string{"test1", "test2"}), raHttp.G(345), diff --git a/models/ticket.go b/models/ticket.go index ece406a..2a894db 100644 --- a/models/ticket.go +++ b/models/ticket.go @@ -90,3 +90,48 @@ type GetMostRecentTicketsRecentTicket struct { ReportStateDescription string `json:"ReportStateDescription"` ReportTypeDescription string `json:"ReportTypeDescription"` } + +type GetGameTicketStatsParameters struct { + // The target game ID + GameID int + + // [Optional] Get unofficial achievements (default: false) + Unofficial *bool + + // [Optional] Return deep ticket metadata in the response's `Tickets` array (default: false) + IncludeTicketMetadata *bool +} + +type GetGameTicketStats struct { + GameID int `json:"GameID"` + GameTitle string `json:"GameTitle"` + ConsoleName string `json:"ConsoleName"` + OpenTickets int `json:"OpenTickets"` + URL string `json:"URL"` + Tickets []GetGameTicketStatsTicket `json:"Tickets"` +} + +type GetGameTicketStatsTicket struct { + ID int `json:"ID"` + AchievementID int `json:"AchievementID"` + AchievementTitle string `json:"AchievementTitle"` + AchievementDesc string `json:"AchievementDesc"` + AchievementType *string `json:"AchievementType"` + Points int `json:"Points"` + BadgeName string `json:"BadgeName"` + AchievementAuthor string `json:"AchievementAuthor"` + GameID int `json:"GameID"` + ConsoleName string `json:"ConsoleName"` + GameTitle string `json:"GameTitle"` + GameIcon string `json:"GameIcon"` + ReportedAt DateTime `json:"ReportedAt"` + ReportType int `json:"ReportType"` + ReportState int `json:"ReportState"` + Hardcore *int `json:"Hardcore"` + ReportNotes string `json:"ReportNotes"` + ReportedBy string `json:"ReportedBy"` + ResolvedAt *DateTime `json:"ResolvedAt"` + ResolvedBy *string `json:"ResolvedBy"` + ReportStateDescription string `json:"ReportStateDescription"` + ReportTypeDescription string `json:"ReportTypeDescription"` +} diff --git a/ticket.go b/ticket.go index e930702..f8a24ae 100644 --- a/ticket.go +++ b/ticket.go @@ -77,3 +77,28 @@ func (c *Client) GetMostRecentTickets(params models.GetMostRecentTicketsParamete } return resp, nil } + +// GetGameTicketStats gets ticket stats for a game, targeted by that game's unique ID. +func (c *Client) GetGameTicketStats(params models.GetGameTicketStatsParameters) (*models.GetGameTicketStats, error) { + details := []raHttp.RequestDetail{ + raHttp.Method(http.MethodGet), + raHttp.Path("/API/API_GetTicketData.php"), + raHttp.APIToken(c.Secret), + raHttp.G(params.GameID), + } + if params.Unofficial != nil && *params.Unofficial { + details = append(details, raHttp.F(5)) + } + if params.IncludeTicketMetadata != nil && *params.IncludeTicketMetadata { + details = append(details, raHttp.D(strconv.Itoa(1))) + } + r, err := c.do(details...) + if err != nil { + return nil, fmt.Errorf("calling endpoint: %w", err) + } + resp, err := raHttp.ResponseObject[models.GetGameTicketStats](r) + if err != nil { + return nil, fmt.Errorf("parsing response object: %w", err) + } + return resp, nil +} diff --git a/ticket_test.go b/ticket_test.go index 6908008..93eb4da 100644 --- a/ticket_test.go +++ b/ticket_test.go @@ -423,6 +423,10 @@ func TestGetMostRecentTickets(tt *testing.T) { require.Equal(t, reportedAt, resp.RecentTickets[0].ReportedAt.Time) require.Equal(t, 0, resp.RecentTickets[0].ReportType) require.Equal(t, 2, resp.RecentTickets[0].ReportState) + require.NotNil(t, resp.RecentTickets[0].Hardcore) + require.Equal(t, hardcore, *resp.RecentTickets[0].Hardcore) + require.NotNil(t, resp.RecentTickets[0].ResolvedAt) + require.Equal(t, resolvedAt, resp.RecentTickets[0].ResolvedAt.Time) require.NotNil(t, resp.RecentTickets[0].ResolvedBy) require.Equal(t, resolvedBy, *resp.RecentTickets[0].ResolvedBy) require.Equal(t, "Resolved", resp.RecentTickets[0].ReportStateDescription) @@ -455,3 +459,183 @@ func TestGetMostRecentTickets(tt *testing.T) { }) } } + +func TestGetGameTicketStats(tt *testing.T) { + unofficial := true + metadata := true + achievementType := "progression" + reportedAt, err := time.Parse(time.DateTime, "2014-02-22 23:23:53") + require.NoError(tt, err) + hardcore := 1 + resolvedAt, err := time.Parse(time.DateTime, "2014-02-24 22:51:10") + require.NoError(tt, err) + resolvedBy := "Scott" + tests := []struct { + name string + params models.GetGameTicketStatsParameters + modifyURL func(url string) string + responseCode int + responseMessage models.GetGameTicketStats + responseError models.ErrorResponse + response func(messageBytes []byte, errorBytes []byte) []byte + assert func(t *testing.T, resp *models.GetGameTicketStats, err error) + }{ + { + name: "fail to call endpoint", + params: models.GetGameTicketStatsParameters{ + GameID: 1, + Unofficial: &unofficial, + IncludeTicketMetadata: &metadata, + }, + modifyURL: func(url string) string { + return "" + }, + responseCode: http.StatusOK, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return messageBytes + }, + assert: func(t *testing.T, resp *models.GetGameTicketStats, err error) { + require.Nil(t, resp) + require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetTicketData.php?d=1&f=5&g=1&y=some_secret\": unsupported protocol scheme \"\"") + }, + }, + { + name: "error response", + params: models.GetGameTicketStatsParameters{ + GameID: 1, + Unofficial: &unofficial, + IncludeTicketMetadata: &metadata, + }, + 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, resp *models.GetGameTicketStats, err error) { + require.Nil(t, resp) + require.EqualError(t, err, "parsing response object: error code 401 returned: {\"message\":\"test\",\"errors\":[{\"status\":401,\"code\":\"unauthorized\",\"title\":\"Not Authorized\"}]}") + }, + }, + { + name: "success", + params: models.GetGameTicketStatsParameters{ + GameID: 1, + Unofficial: &unofficial, + IncludeTicketMetadata: &metadata, + }, + modifyURL: func(url string) string { + return url + }, + responseCode: http.StatusOK, + responseMessage: models.GetGameTicketStats{ + GameID: 20, + GameTitle: "Alex Kidd in the Enchanted Castle", + ConsoleName: "Genesis/Mega Drive", + OpenTickets: 4, + URL: "https://retroachievements.org/game/20/tickets", + Tickets: []models.GetGameTicketStatsTicket{ + { + ID: 1, + AchievementID: 3778, + AchievementTitle: "Exhibition Match", + AchievementDesc: "Complete Stage 3: Flugelheim Museum", + AchievementType: &achievementType, + Points: 10, + BadgeName: "04357", + AchievementAuthor: "Batman", + GameID: 45, + ConsoleName: "Genesis/Mega Drive", + GameTitle: "Batman: The Video Game", + GameIcon: "/Images/053393.png", + ReportedAt: models.DateTime{ + Time: reportedAt, + }, + ReportType: 0, + ReportState: 2, + Hardcore: &hardcore, + ReportNotes: "This achievement didn't trigger for some reason?", + ReportedBy: "qwe", + ResolvedAt: &models.DateTime{ + Time: resolvedAt, + }, + ResolvedBy: &resolvedBy, + ReportStateDescription: "Resolved", + ReportTypeDescription: "Invalid ticket type", + }, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return messageBytes + }, + assert: func(t *testing.T, resp *models.GetGameTicketStats, err error) { + require.NotNil(t, resp) + require.Equal(t, 20, resp.GameID) + require.Equal(t, "Alex Kidd in the Enchanted Castle", resp.GameTitle) + require.Equal(t, "Genesis/Mega Drive", resp.ConsoleName) + require.Equal(t, 4, resp.OpenTickets) + require.Equal(t, "https://retroachievements.org/game/20/tickets", resp.URL) + require.Len(t, resp.Tickets, 1) + require.Equal(t, 1, resp.Tickets[0].ID) + require.Equal(t, 3778, resp.Tickets[0].AchievementID) + require.Equal(t, "Exhibition Match", resp.Tickets[0].AchievementTitle) + require.Equal(t, "Complete Stage 3: Flugelheim Museum", resp.Tickets[0].AchievementDesc) + require.NotNil(t, resp.Tickets[0].AchievementType) + require.Equal(t, achievementType, *resp.Tickets[0].AchievementType) + require.Equal(t, 10, resp.Tickets[0].Points) + require.Equal(t, "04357", resp.Tickets[0].BadgeName) + require.Equal(t, "Batman", resp.Tickets[0].AchievementAuthor) + require.Equal(t, 45, resp.Tickets[0].GameID) + require.Equal(t, "Genesis/Mega Drive", resp.Tickets[0].ConsoleName) + require.Equal(t, "Batman: The Video Game", resp.Tickets[0].GameTitle) + require.Equal(t, "/Images/053393.png", resp.Tickets[0].GameIcon) + require.Equal(t, reportedAt, resp.Tickets[0].ReportedAt.Time) + require.Equal(t, 0, resp.Tickets[0].ReportType) + require.Equal(t, 2, resp.Tickets[0].ReportState) + require.NotNil(t, resp.Tickets[0].Hardcore) + require.Equal(t, hardcore, *resp.Tickets[0].Hardcore) + require.NotNil(t, resp.Tickets[0].ResolvedAt) + require.Equal(t, resolvedAt, resp.Tickets[0].ResolvedAt.Time) + require.NotNil(t, resp.Tickets[0].ResolvedBy) + require.Equal(t, resolvedBy, *resp.Tickets[0].ResolvedBy) + require.Equal(t, "Resolved", resp.Tickets[0].ReportStateDescription) + require.Equal(t, "Invalid ticket type", resp.Tickets[0].ReportTypeDescription) + 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_GetTicketData.php" + if r.URL.Path != expectedPath { + t.Errorf("Expected to request '%s', got: %s", expectedPath, r.URL.Path) + } + w.WriteHeader(test.responseCode) + messageBytes, err := json.Marshal(test.responseMessage) + require.NoError(t, err) + errBytes, err := json.Marshal(test.responseError) + require.NoError(t, err) + resp := test.response(messageBytes, 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), "go-retroachievements/v0.0.0", "some_secret") + resp, err := client.GetGameTicketStats(test.params) + test.assert(t, resp, err) + }) + } +} diff --git a/user.go b/user.go index 3770644..6b7a296 100644 --- a/user.go +++ b/user.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "time" raHttp "github.com/joshraphael/go-retroachievements/http" "github.com/joshraphael/go-retroachievements/models" @@ -76,7 +77,7 @@ func (c *Client) GetAchievementsEarnedOnDay(params models.GetAchievementsEarnedO raHttp.Path("/API/API_GetAchievementsEarnedOnDay.php"), raHttp.APIToken(c.Secret), raHttp.U(params.Username), - raHttp.D(params.Date), + raHttp.D(params.Date.UTC().Format(time.DateOnly)), ) if err != nil { return nil, fmt.Errorf("calling endpoint: %w", err)