Repository: nicklaw5/helix
Pull request #87 (2021-05-06 21:33:10)

Add get/create/end poll endpoints
First part of Twitch changelog (2021‑05‑03)


Patch:
diff --git a/README.md b/README.md
index 42b698d..8451d54 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,12 @@ looking for the Twitch API docs, see the [Twitch Developer website](https://dev.
 - [x] Get Banned Users
 - [ ] Get Moderators
 - [ ] Get Moderator Events
+- [x] Get Polls         {beta}
+- [x] Create Poll       {beta}
+- [x] End Poll          {beta}
+- [ ] Get Predictions   {beta}
+- [ ] Create Prediction {beta}
+- [ ] End Prediction    {beta}
 - [ ] Search Categories
 - [ ] Search Channels
 - [ ] Get Stream Key
diff --git a/docs/polls_docs.md b/docs/polls_docs.md
new file mode 100644
index 0000000..ec3bcdc
--- /dev/null
+++ b/docs/polls_docs.md
@@ -0,0 +1,76 @@
+# Polls Documentation
+
+## Get Polls
+
+This is an example of how to get polls.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetPolls(&helix.PollsParams{
+    BroadcasterID: "145328278",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Create Poll
+
+This is an example of how to create a poll.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.CreatePoll(&helix.CreatePollParams{
+    BroadcasterID: "145328278",
+    Title: "Test",
+    Choices: []helix.ChoiceParam{
+        helix.ChoiceParam{ Title: "choice 1" },
+        helix.ChoiceParam{ Title: "choice 2" },
+    },
+    Duration: 30,
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## End Poll
+
+This is an example of how to end a poll.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.EndPoll(&helix.EndPollParams{
+    BroadcasterID: "145328278",
+    ID: "25b14b42-d4d8-4756-86ce-842bf76f82a0",
+    Status: "TERMINATED",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
diff --git a/eventsub.go b/eventsub.go
index 31abcc7..b23075c 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -81,6 +81,7 @@ const (
 	EventSubTypeChannelUpdate                             = "channel.update"
 	EventSubTypeChannelFollow                             = "channel.follow"
 	EventSubTypeChannelSubscription                       = "channel.subscribe"
+	EventSubTypeChannelUnsubscribe                        = "channel.unsubscribe" /* beta */
 	EventSubTypeChannelCheer                              = "channel.cheer"
 	EventSubTypeChannelRaid                               = "channel.raid"
 	EventSubTypeChannelBan                                = "channel.ban"
@@ -92,6 +93,13 @@ const (
 	EventSubTypeChannelPointsCustomRewardRemove           = "channel.channel_points_custom_reward.remove"
 	EventSubTypeChannelPointsCustomRewardRedemptionAdd    = "channel.channel_points_custom_reward_redemption.add"
 	EventSubTypeChannelPointsCustomRewardRedemptionUpdate = "channel.channel_points_custom_reward_redemption.update"
+	EventSubTypeChannelPollBegin                          = "channel.poll.begin"          /* beta */
+	EventSubTypeChannelPollProgress                       = "channel.poll.progress"       /* beta */
+	EventSubTypeChannelPollEnd                            = "channel.poll.end"            /* beta */
+	EventSubTypeChannelPredictionBegin                    = "channel.prediction.begin"    /* beta */
+	EventSubTypeChannelPredictionProgress                 = "channel.prediction.progress" /* beta */
+	EventSubTypeChannelPredictionLock                     = "channel.prediction.lock"     /* beta */
+	EventSubTypeChannelPredictionEnd                      = "channel.prediction.end"      /* beta */
 	EventSubTypeHypeTrainBegin                            = "channel.hype_train.begin"
 	EventSubTypeHypeTrainProgress                         = "channel.hype_train.progress"
 	EventSubTypeHypeTrainEnd                              = "channel.hype_train.end"
@@ -196,6 +204,48 @@ type EventSubChannelRaidEvent struct {
 	Viewers                  int    `json:"viewers"`
 }
 
+// Data for a channel poll begin event
+type EventSubChannelPollBeginEvent struct {
+	ID                      string                      `json:"id"`
+	BroadcasterUserID       string                      `json:"broadcaster_user_id"`
+	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"`
+	ChannelPointsVoting     EventSubChannelPointsVoting `json:"channel_points_voting"`
+	StartedAt               Time                        `json:"started_at"`
+	EndsAt                  Time                        `json:"ends_at"`
+}
+
+// Data for a channel poll progress event, it's the same as the channel poll begin event
+type EventSubChannelPollProgressEvent = EventSubChannelPollBeginEvent
+
+// Data for a channel poll end event
+type EventSubChannelPollEndEvent struct {
+	ID                      string                      `json:"id"`
+	BroadcasterUserID       string                      `json:"broadcaster_user_id"`
+	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"`
+	ChannelPointsVoting     EventSubChannelPointsVoting `json:"channel_points_voting"`
+	Status                  string                      `json:"status"`
+	StartedAt               Time                        `json:"started_at"`
+	EndedAt                 Time                        `json:"ended_at"`
+}
+
+// EventSubBitVoting ...
+type EventSubBitVoting struct {
+	IsEnabled     bool `json:"is_enabled"`
+	AmountPerVote int  `json:"amount_per_vote"`
+}
+
+// ChannelPointsVoting ...
+type EventSubChannelPointsVoting = EventSubBitVoting
+
+
 // Data for a channel points custom reward notification
 type EventSubChannelPointsCustomRewardEvent struct {
 	ID                                string                 `json:"id"`
@@ -235,6 +285,39 @@ type EventSubChannelPointsCustomRewardRedemptionEvent struct {
 	RedeemedAt           Time           `json:"redeemed_at"`
 }
 
+// Data for a channel channel prediction begin event
+type EventSubChannelPredictionBeginEvent struct {
+	ID                   string            `json:"id"`
+	BroadcasterUserID    string            `json:"broadcaster_user_id"`
+	BroadcasterUserLogin string            `json:"broadcaster_user_login"`
+	BroadcasterUserName  string            `json:"broadcaster_user_name"`
+	Title                string            `json:"title"`
+	Outcomes             []EventSubOutcome `json:"outcomes"`
+	StartedAt            Time              `json:"started_at"`
+	LocksAt              Time              `json:"outcomes"`
+}
+
+// Data for a channel channel prediction progress event
+type EventSubChannelPredictionProgressEvent = EventSubChannelPredictionBeginEvent
+
+// Data for a channel channel prediction lock event
+type EventSubChannelPredictionLockEvent = EventSubChannelPredictionBeginEvent
+
+// Data for a channel channel prediction end event
+type EventSubChannelPredictionEndEvent struct {
+	ID                   string            `json:"id"`
+	BroadcasterUserID    string            `json:"broadcaster_user_id"`
+	BroadcasterUserLogin string            `json:"broadcaster_user_login"`
+	BroadcasterUserName  string            `json:"broadcaster_user_name"`
+	Title                string            `json:"title"`
+	WinningOutcomeID     string            `json:"winning_outcome_id"`
+	Outcomes             []EventSubOutcome `json:"outcomes"`
+	Status               string            `json:"status"`
+	StartedAt            Time              `json:"started_at"`
+	LocksAt              Time              `json:"outcomes"`
+	EndedAt              Time              `json:"eneded_at"`
+}
+
 // Data for a hype train begin notification
 type EventSubHypeTrainBeginEvent struct {
 	BroadcasterUserID    string                 `json:"broadcaster_user_id"`
@@ -333,12 +416,31 @@ type EventSubContribution struct {
 	Total     string `json:"total"`
 }
 
+// This belong to an outcome and defines user reward
+type EventSubTopPredictor struct {
+	UserID            string `json:"user_id"`
+	UserLogin         string `json:"user_login"`
+	UserName          string `json:"user_name"`
+	ChannelPointWon   string `json:"channel_points_won"`
+	ChannelPointsUsed int    `json:"channel_points_used"`
+}
+
 // This belongs to a custom reward and defines if it is limited per stream
 type EventSubMaxPerStream struct {
 	IsEnabled bool `json:"is_enabled"`
 	Value     int  `json:"value"`
 }
 
+// This belong to a channel prediction and defines the outcomes
+type EventSubOutcome struct {
+	ID            string                 `json:"id"`
+	Title         string                 `json:"title"`
+	Color         string                 `json:"color"`
+	Users         int                    `json:"users"`
+	ChannelPoints int                    `json:"channel_points"`
+	TopPredictors []EventSubTopPredictor `json:"top_predictors"`
+}
+
 // This belongs to a reward redemption and defines the reward redeemed
 type EventSubReward struct {
 	ID     string `json:"id"`
diff --git a/polls.go b/polls.go
new file mode 100644
index 0000000..1158150
--- /dev/null
+++ b/polls.go
@@ -0,0 +1,124 @@
+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"`
+	Title                      string              `json:"title"`
+	Choices                    []Choice            `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"`
+	StartedAt                  Time                `json:"started_at"`
+	EndedAt                     Time               `json:"ended_at"`
+}
+
+// Choice ...
+type Choice struct {
+	ID                 string `json:"id"`
+	Title              string `json:"title"`
+	BitVotes           int    `json:"bit_votes"`
+	ChannelPointsVotes int    `json:"channel_points_votes"`
+	Votes              int    `json:"votes"`
+}
+
+// ManyPolls ...
+type ManyPolls struct {
+	Polls []Poll `json:"data"`
+	Pagination Pagination `json:"pagination"`
+}
+
+// PollsResponse ...
+type PollsResponse struct {
+	ResponseCommon
+	Data ManyPolls
+}
+
+// PollsParams ...
+type PollsParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+}
+
+// GetPollsResponse ...
+type GetPollsResponse struct {
+	ResponseCommon
+	Data ManyPolls
+}
+
+// GetPolls ...
+// Required scope: channel:read:polls
+func (c *Client) GetPolls(params *PollsParams) (*PollsResponse, error) {
+	resp, err := c.get("/polls", &ManyPolls{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	polls := &PollsResponse{}
+	resp.HydrateResponseCommon(&polls.ResponseCommon)
+	polls.Data.Polls = resp.Data.(*ManyPolls).Polls
+	polls.Data.Pagination = resp.Data.(*ManyPolls).Pagination
+
+	return polls, nil
+}
+
+// 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.
+}
+
+// ChoiceParam ...
+type ChoiceParam struct {
+	Title string `json:"title"` // Maximum: 25 characters.
+}
+
+// CreatePoll ...
+// Required scope: channel:manage:polls
+func (c *Client) CreatePoll(params *CreatePollParams) (*PollsResponse, error) {
+	resp, err := c.postAsJSON("/polls", &ManyPolls{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	polls := &PollsResponse{}
+	resp.HydrateResponseCommon(&polls.ResponseCommon)
+	polls.Data.Polls = resp.Data.(*ManyPolls).Polls
+	polls.Data.Pagination = resp.Data.(*ManyPolls).Pagination
+
+	return polls, nil
+}
+
+// EndPollParams ...
+type EndPollParams struct {
+	BroadcasterID string `json:"broadcaster_id"`
+	ID            string `json:"id"`
+	Status        string `json:"status"`
+}
+
+// EndPoll ...
+// Required scope: channel:manage:polls
+func (c *Client) EndPoll(params *EndPollParams) (*PollsResponse, error) {
+	resp, err := c.patchAsJSON("/polls", &ManyPolls{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	polls := &PollsResponse{}
+	resp.HydrateResponseCommon(&polls.ResponseCommon)
+	polls.Data.Polls = resp.Data.(*ManyPolls).Polls
+	polls.Data.Pagination = resp.Data.(*ManyPolls).Pagination
+
+	return polls, nil
+}
+
+


Test patch:
diff --git a/polls_test.go b/polls_test.go
new file mode 100644
index 0000000..4dc8667
--- /dev/null
+++ b/polls_test.go
@@ -0,0 +1,241 @@
+package helix
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestGetPolls(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode   int
+		options      *Options
+		PollsParams  *PollsParams
+		respBody     string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&PollsParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&PollsParams{BroadcasterID: "121445595"},
+			`{"data":[{"id":"ed961efd-8a3f-4cf5-a9d0-e616c590cd2a","broadcaster_id":"55696719","broadcaster_name":"TwitchDev","broadcaster_login":"twitchdev","title":"Heads or Tails?","choices":[{"id": "4c123012-1351-4f33-84b7-43856e7a0f47","title": "Heads","votes": 0,"channel_points_votes": 0,"bits_votes": 0},{"id": "279087e3-54a7-467e-bcd0-c1393fcea4f0","title": "Tails","votes": 0,"channel_points_votes": 0,"bits_votes": 0}],"bits_voting_enabled": false,"bits_per_vote": 0,"channel_points_voting_enabled": false,"channel_points_per_vote": 0,"status": "ACTIVE","duration": 1800,"started_at": "2021-03-19T06:08:33.871278372Z"}],"pagination": {}}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetPolls(testCase.PollsParams)
+		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.GetPolls(&PollsParams{})
+	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 TestCreatePoll(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode       int
+		options          *Options
+		CreatePollParams *CreatePollParams
+		respBody         string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&CreatePollParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&CreatePollParams{
+				BroadcasterID: "145328278",
+				Title: "Test",
+				Choices: []ChoiceParam{
+					ChoiceParam{ Title: "choix 1" },
+					ChoiceParam{ Title: "choix 2" },
+				},
+				Duration: 30,
+			},
+			`{"data":[{"id":"fb156390-a4de-4acb-8ca6-52ef125f533b","broadcaster_id":"145328278","broadcaster_name":"Scorfly","broadcaster_login":"scorfly","title":"Test","choices":[{"id":"3fea0835-6059-4bb2-95ab-1318659f0282","title":"choix 1","votes":0,"channel_points_votes":0,"bits_votes":0},{"id":"fc4d6457-f32b-492b-a93e-aaa88c343598","title":"choix 2","votes":0,"channel_points_votes":0,"bits_votes":0}],"bits_voting_enabled":false,"bits_per_vote":0,"channel_points_voting_enabled":false,"channel_points_per_vote":0,"status":"ACTIVE","duration":30,"started_at":"2021-05-06T20:43:24.60506479Z"}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.CreatePoll(testCase.CreatePollParams)
+		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.CreatePoll(&CreatePollParams{})
+	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 TestEndPoll(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode    int
+		options       *Options
+		EndPollParams *EndPollParams
+		respBody      string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&EndPollParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&EndPollParams{
+				BroadcasterID: "145328278",
+				ID: "25b14b42-d4d8-4756-86ce-842bf76f82a0",
+				Status: "TERMINATED",
+			},
+			`{"data":[{"id":"6aee6aae-e536-4eb0-afa5-d64567aec2c6","broadcaster_id":"145328278","broadcaster_name":"Scorfly","broadcaster_login":"scorfly","title":"Test","choices":[{"id":"cdebad56-ea5a-4d7a-8caf-cdf80c71514e","title":"choix 1","votes":0,"channel_points_votes":0,"bits_votes":0},{"id":"e3027452-e3ab-4ee4-bc4a-03d9bac37dcc","title":"choix 2","votes":0,"channel_points_votes":0,"bits_votes":0}],"bits_voting_enabled":false,"bits_per_vote":0,"channel_points_voting_enabled":false,"channel_points_per_vote":0,"status":"TERMINATED","duration":300,"started_at":"2021-05-06T21:15:08.661352925Z","ended_at":"2021-05-06T21:15:26.894542904Z"}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.EndPoll(testCase.EndPollParams)
+		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.EndPoll(&EndPollParams{})
+	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")
+	}
+}