Repository: nicklaw5/helix
Pull request #88 (2021-05-07 22:31:20)

Add get / create / end prediction features
Add few fix on polls keys

End of second part of 2021‑05‑03 changelog


Patch:
diff --git a/docs/polls_docs.md b/docs/polls_docs.md
index ec3bcdc..ce3e0b6 100644
--- a/docs/polls_docs.md
+++ b/docs/polls_docs.md
@@ -37,9 +37,9 @@ if err != nil {
 resp, err := client.CreatePoll(&helix.CreatePollParams{
     BroadcasterID: "145328278",
     Title: "Test",
-    Choices: []helix.ChoiceParam{
-        helix.ChoiceParam{ Title: "choice 1" },
-        helix.ChoiceParam{ Title: "choice 2" },
+    Choices: []helix.PollChoiceParam{
+        helix.PollChoiceParam{ Title: "choice 1" },
+        helix.PollChoiceParam{ Title: "choice 2" },
     },
     Duration: 30,
 })
diff --git a/docs/predictions_docs.md b/docs/predictions_docs.md
new file mode 100644
index 0000000..0615e9f
--- /dev/null
+++ b/docs/predictions_docs.md
@@ -0,0 +1,77 @@
+# Predictions Documentation
+
+## Get Predictions
+
+This is an example of how to get predictions.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetPredictions(&helix.PredictionsParams{
+    BroadcasterID: "145328278",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Create Prediction
+
+This is an example of how to create a prediction.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.CreatePrediction(&helix.CreatePredictionParams{
+    BroadcasterID: "145328278",
+    Title: "Test",
+    Outcomes: []helix.PredictionChoiceParam{
+        helix.PredictionChoiceParam{ Title: "choice 1" },
+        helix.PredictionChoiceParam{ Title: "choice 2" },
+    },
+    PredictionWindow: 300,
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## End Prediction
+
+This is an example of how to end a prediction.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.EndPrediction(&helix.EndPredictionParams{
+    BroadcasterID: "145328278",
+    ID: "c36165d9-d5f5-4f81-ab56-17e7347110c8",
+    Status: "RESOLVED",
+    WinningOutcomeID: "d0c0194a-6016-4ca3-b8eb-0c61183758ab",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
diff --git a/eventsub.go b/eventsub.go
index b23075c..b9dcd2a 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -211,8 +211,8 @@ type EventSubChannelPollBeginEvent struct {
 	BroadcasterUserLogin    string                      `json:"broadcaster_user_login"`
 	BroadcasterUserName     string                      `json:"broadcaster_user_name"`
 	Title                   string                      `json:"title"`
-	Choices                 []Choice                    `json:"choices"`
-	BitVoting               EventSubBitVoting           `json:"bits_voting"`
+	Choices                 []PollChoice                `json:"choices"`
+	BitsVoting              EventSubBitVoting           `json:"bits_voting"`
 	ChannelPointsVoting     EventSubChannelPointsVoting `json:"channel_points_voting"`
 	StartedAt               Time                        `json:"started_at"`
 	EndsAt                  Time                        `json:"ends_at"`
@@ -228,8 +228,8 @@ type EventSubChannelPollEndEvent struct {
 	BroadcasterUserLogin    string                      `json:"broadcaster_user_login"`
 	BroadcasterUserName     string                      `json:"broadcaster_user_name"`
 	Title                   string                      `json:"title"`
-	Choices                 []Choice                    `json:"choices"`
-	BitVoting               EventSubBitVoting           `json:"bits_voting"`
+	Choices                 []PollChoice                `json:"choices"`
+	BitsVoting              EventSubBitVoting           `json:"bits_voting"`
 	ChannelPointsVoting     EventSubChannelPointsVoting `json:"channel_points_voting"`
 	Status                  string                      `json:"status"`
 	StartedAt               Time                        `json:"started_at"`
diff --git a/polls.go b/polls.go
index 1158150..ebff117 100644
--- a/polls.go
+++ b/polls.go
@@ -3,25 +3,26 @@ package helix
 // Poll ...
 type Poll struct {
 	ID                         string              `json:"id"`
-	BroadcasterUserID          string              `json:"broadcaster_user_id"`
-	BroadcasterUserLogin       string              `json:"broadcaster_user_login"`
-	BroadcasterUserName        string              `json:"broadcaster_user_name"`
+	BroadcasterID              string              `json:"broadcaster_id"`
+	BroadcasterLogin           string              `json:"broadcaster_login"`
+	BroadcasterName            string              `json:"broadcaster_name"`
 	Title                      string              `json:"title"`
-	Choices                    []Choice            `json:"choices"`
+	Choices                    []PollChoice        `json:"choices"`
 	BitsVotingEnabled          bool                `json:"bits_voting_enabled"`
 	BitsPerVote                int                 `json:"bits_per_vote"`
 	ChannelPointsVotingEnabled bool                `json:"channel_points_voting_enabled"`
 	ChannelPointsPerVote       int                 `json:"channel_points_per_vote"`
 	Status                     string              `json:"status"`
+	Duration                   int                 `json:"duration"`
 	StartedAt                  Time                `json:"started_at"`
-	EndedAt                     Time               `json:"ended_at"`
+	EndedAt                    Time                `json:"ended_at"`
 }
 
-// Choice ...
-type Choice struct {
+// PollChoice ...
+type PollChoice struct {
 	ID                 string `json:"id"`
 	Title              string `json:"title"`
-	BitVotes           int    `json:"bit_votes"`
+	BitsVotes          int    `json:"bits_votes"`
 	ChannelPointsVotes int    `json:"channel_points_votes"`
 	Votes              int    `json:"votes"`
 }
@@ -41,6 +42,9 @@ type PollsResponse struct {
 // PollsParams ...
 type PollsParams struct {
 	BroadcasterID string `query:"broadcaster_id"`
+	ID            string `query:"id"`
+	After         string `query:"after"`
+	First         string `query:"first`
 }
 
 // GetPollsResponse ...
@@ -67,18 +71,18 @@ func (c *Client) GetPolls(params *PollsParams) (*PollsResponse, error) {
 
 // CreatePollParams ...
 type CreatePollParams struct {
-	BroadcasterID              string        `json:"broadcaster_id"`
-	Title                      string        `json:"title"`                         // Maximum: 60 characters.
-	Choices                    []ChoiceParam `json:"choices"`                       // Minimum: 2 choices. Maximum: 5 choices.
-	Duration                   int           `json:"duration"`                      // Minimum: 15. Maximum: 1800.
-	BitsVotingEnabled          bool          `json:"bits_voting_enabled"`           // Default: false
-	BitsPerVote                int           `json:"bits_per_vote"`                 // Minimum: 0. Maximum: 10000.
-	ChannelPointsVotingEnabled bool          `json:"channel_points_voting_enabled"` // Default: false
-	ChannelPointsPerVote       int           `json:"channel_points_per_vote"`       // Minimum: 0. Maximum: 1000000.
+	BroadcasterID              string            `json:"broadcaster_id"`
+	Title                      string            `json:"title"`                         // Maximum: 60 characters.
+	Choices                    []PollChoiceParam `json:"choices"`                       // Minimum: 2 choices. Maximum: 5 choices.
+	Duration                   int               `json:"duration"`                      // Minimum: 15. Maximum: 1800.
+	BitsVotingEnabled          bool              `json:"bits_voting_enabled"`           // Default: false
+	BitsPerVote                int               `json:"bits_per_vote"`                 // Minimum: 0. Maximum: 10000.
+	ChannelPointsVotingEnabled bool              `json:"channel_points_voting_enabled"` // Default: false
+	ChannelPointsPerVote       int               `json:"channel_points_per_vote"`       // Minimum: 0. Maximum: 1000000.
 }
 
-// ChoiceParam ...
-type ChoiceParam struct {
+// PollChoiceParam ...
+type PollChoiceParam struct {
 	Title string `json:"title"` // Maximum: 25 characters.
 }
 
diff --git a/predictions.go b/predictions.go
new file mode 100644
index 0000000..84f04fd
--- /dev/null
+++ b/predictions.go
@@ -0,0 +1,133 @@
+package helix
+
+// Prediction ... same struct as Poll
+type Prediction struct {
+	ID                   string     `json:"id"`
+	BroadcasterUserID    string     `json:"broadcaster_id"`
+	BroadcasterUserLogin string     `json:"broadcaster_login"`
+	BroadcasterUserName  string     `json:"broadcaster_name"`
+	Title                string     `json:"title"`
+	WinningOutcomeID     string     `json:"winning_outcome_id"`
+	Outcomes             []Outcomes `json:"outcomes"`
+	PredictionWindow     int        `json:"prediction_window"`
+	Status               string     `json:"status"`
+	CreatedAt            Time       `json:"created_at"`
+	EndedAt              Time       `json:"ended_at"`
+	LockedAt             Time       `json:"locked_at"`
+}
+
+// Outcomes ...
+type Outcomes struct {
+	ID              string         `json:"id"`
+	Title           string         `json:"title"`
+	Users           int            `json:"users"`
+	ChannelPoints   int            `json:"channel_points"`
+	TopPredictors   []TopPredictor `json:"top_predictors"`
+	Color           string         `json:"color"`
+}
+
+// TopPredictor ...
+type TopPredictor struct {
+	UserID             string `json:"user_id"`
+	UserName           string `json:"user_name"`
+	UserLogin          string `json:"user_login"`
+	ChannelPointsUsed  int    `json:"channel_points_used"`
+	ChannelPointsWon   int    `json:"channel_points_won"`
+}
+
+// ManyPredictions ...
+type ManyPredictions struct {
+	Predictions []Prediction `json:"data"`
+	Pagination Pagination `json:"pagination"`
+}
+
+// PredictionsResponse ...
+type PredictionsResponse struct {
+	ResponseCommon
+	Data ManyPredictions
+}
+
+// PredictionsParams ...
+type PredictionsParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+	ID            string `query:"id"`
+	After         string `query:"after"`
+	First         string `query:"first`
+}
+
+// GetPredictionsResponse ...
+type GetPredictionsResponse struct {
+	ResponseCommon
+	Data ManyPredictions
+}
+
+// GetPredictions ...
+// Required scope: channel:read:predictions
+func (c *Client) GetPredictions(params *PredictionsParams) (*PredictionsResponse, error) {
+	resp, err := c.get("/predictions", &ManyPredictions{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	predictions := &PredictionsResponse{}
+	resp.HydrateResponseCommon(&predictions.ResponseCommon)
+	predictions.Data.Predictions = resp.Data.(*ManyPredictions).Predictions
+	predictions.Data.Pagination = resp.Data.(*ManyPredictions).Pagination
+
+	return predictions, nil
+}
+
+// CreatePredictionParams ...
+type CreatePredictionParams struct {
+	BroadcasterID    string                  `json:"broadcaster_id"`
+	Title            string                  `json:"title"`             // Maximum: 45 characters.
+	Outcomes         []PredictionChoiceParam `json:"outcomes"`          // 2 choices mandatory
+	PredictionWindow int                     `json:"prediction_window"` // Minimum: 1. Maximum: 1800.
+}
+
+// PredictionChoiceParam ...
+type PredictionChoiceParam struct {
+	Title string `json:"title"` // Maximum: 25 characters.
+}
+
+// CreatePrediction ...
+// Required scope: channel:manage:predictions
+func (c *Client) CreatePrediction(params *CreatePredictionParams) (*PredictionsResponse, error) {
+	resp, err := c.postAsJSON("/predictions", &ManyPredictions{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	predictions := &PredictionsResponse{}
+	resp.HydrateResponseCommon(&predictions.ResponseCommon)
+	predictions.Data.Predictions = resp.Data.(*ManyPredictions).Predictions
+	predictions.Data.Pagination = resp.Data.(*ManyPredictions).Pagination
+
+	return predictions, nil
+}
+
+// EndPredictionParams ...
+type EndPredictionParams struct {
+	BroadcasterID    string `json:"broadcaster_id"`
+	ID               string `json:"id"`
+	Status           string `json:"status"`
+	WinningOutcomeID string `json:"winning_outcome_id"`
+}
+
+// EndPrediction ...
+// Required scope: channel:manage:predictions
+func (c *Client) EndPrediction(params *EndPredictionParams) (*PredictionsResponse, error) {
+	resp, err := c.patchAsJSON("/predictions", &ManyPredictions{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	predictions := &PredictionsResponse{}
+	resp.HydrateResponseCommon(&predictions.ResponseCommon)
+	predictions.Data.Predictions = resp.Data.(*ManyPredictions).Predictions
+	predictions.Data.Pagination = resp.Data.(*ManyPredictions).Pagination
+
+	return predictions, nil
+}
+
+


Test patch:
diff --git a/polls_test.go b/polls_test.go
index 4dc8667..2bfcf12 100644
--- a/polls_test.go
+++ b/polls_test.go
@@ -100,9 +100,9 @@ func TestCreatePoll(t *testing.T) {
 			&CreatePollParams{
 				BroadcasterID: "145328278",
 				Title: "Test",
-				Choices: []ChoiceParam{
-					ChoiceParam{ Title: "choix 1" },
-					ChoiceParam{ Title: "choix 2" },
+				Choices: []PollChoiceParam{
+					PollChoiceParam{ Title: "choix 1" },
+					PollChoiceParam{ Title: "choix 2" },
 				},
 				Duration: 30,
 			},
diff --git a/predictions_test.go b/predictions_test.go
new file mode 100644
index 0000000..39bda57
--- /dev/null
+++ b/predictions_test.go
@@ -0,0 +1,242 @@
+package helix
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestGetPredictions(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode        int
+		options           *Options
+		PredictionsParams *PredictionsParams
+		respBody          string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&PredictionsParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&PredictionsParams{BroadcasterID: "121445595"},
+			`{"data":[{"id":"0eb6f04e-8867-4f9f-8bdd-e4038556d0e8","broadcaster_id":"145328278","broadcaster_name":"Scorfly","broadcaster_login":"scorfly","title":"test 1","winning_outcome_id":"760f8303-5a4f-420e-8649-527752447e0f","outcomes":[{"id":"760f8303-5a4f-420e-8649-527752447e0f","title":"choice blue","users":1,"channel_points":10,"top_predictors":[{"user_id":"250117050","user_login":"botvause","user_name":"BotVause","channel_points_used":10,"channel_points_won":10}],"color":"BLUE"},{"id":"6c0fe617-1309-4bb7-ad03-9c1b232f2251","title":"choice pink","users":0,"channel_points":0,"top_predictors":null,"color":"PINK"}],"prediction_window":30,"status":"RESOLVED","created_at":"2021-05-07T21:30:28.20509235Z","ended_at":"2021-05-07T21:32:12.402517544Z","locked_at":"2021-05-07T21:30:57.242055129Z"}],"pagination":{}}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetPredictions(testCase.PredictionsParams)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.GetPredictions(&PredictionsParams{})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+		t.Error("expected error does match return error")
+	}
+}
+
+func TestCreatePrediction(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode       int
+		options          *Options
+		CreatePredictionParams *CreatePredictionParams
+		respBody         string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&CreatePredictionParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&CreatePredictionParams{
+				BroadcasterID: "145328278",
+				Title: "Test",
+				Outcomes: []PredictionChoiceParam{
+					PredictionChoiceParam{ Title: "Panda" },
+					PredictionChoiceParam{ Title: "Tiger" },
+				},
+				PredictionWindow: 300,
+			},
+			`{"data":[{"id":"92bdcb5c-6d83-4c75-95d6-fdd34f128d43","broadcaster_id":"145328278","broadcaster_name":"Scorfly","broadcaster_login":"scorfly","title":"Test","winning_outcome_id":null,"outcomes":[{"id":"6afe5daf-e54c-48d7-9c57-d07e791c496b","title":"choix 1","users":0,"channel_points":0,"top_predictors":null,"color":"BLUE"},{"id":"d8ffec60-f87f-44ac-b7b1-c53001bf2e4b","title":"choix 2","users":0,"channel_points":0,"top_predictors":null,"color":"PINK"}],"prediction_window":300,"status":"ACTIVE","created_at":"2021-05-07T22:15:34.457301028Z","ended_at":null,"locked_at":null}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.CreatePrediction(testCase.CreatePredictionParams)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.CreatePrediction(&CreatePredictionParams{})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+		t.Error("expected error does match return error")
+	}
+}
+
+
+func TestEndPrediction(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		EndPredictionParams *EndPredictionParams
+		respBody      string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&EndPredictionParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&EndPredictionParams{
+				BroadcasterID: "145328278",
+				ID: "92bdcb5c-6d83-4c75-95d6-fdd34f128d43",
+				Status: "RESOLVED",
+				WinningOutcomeID: "6afe5daf-e54c-48d7-9c57-d07e791c496b",
+			},
+			`{"data":[{"id":"92bdcb5c-6d83-4c75-95d6-fdd34f128d43","broadcaster_id":"145328278","broadcaster_name":"Scorfly","broadcaster_login":"scorfly","title":"Test","winning_outcome_id":"6afe5daf-e54c-48d7-9c57-d07e791c496b","outcomes":[{"id":"6afe5daf-e54c-48d7-9c57-d07e791c496b","title":"choix 1","users":0,"channel_points":0,"top_predictors":null,"color":"BLUE"},{"id":"d8ffec60-f87f-44ac-b7b1-c53001bf2e4b","title":"choix 2","users":0,"channel_points":0,"top_predictors":null,"color":"PINK"}],"prediction_window":300,"status":"RESOLVED","created_at":"2021-05-07T22:15:34.457301028Z","ended_at":"2021-05-07T22:18:14.015776526Z","locked_at":null}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.EndPrediction(testCase.EndPredictionParams)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode)
+		}
+
+		if resp.StatusCode == http.StatusBadRequest {
+			if resp.Error != "Bad Request" {
+				t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusBadRequest {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "Missing required parameter \"broadcaster_id\""
+			if resp.ErrorMessage != expectedErrMsg {
+				t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage)
+			}
+
+			continue
+		}
+	}
+
+	// Test with HTTP Failure
+	options := &Options{
+		ClientID: "my-client-id",
+		HTTPClient: &badMockHTTPClient{
+			newMockHandler(0, "", nil),
+		},
+	}
+	c := &Client{
+		opts: options,
+	}
+
+	_, err := c.EndPrediction(&EndPredictionParams{})
+	if err == nil {
+		t.Error("expected error but got nil")
+	}
+
+	if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+		t.Error("expected error does match return error")
+	}
+}