Repository: nicklaw5/helix
Pull request #96 (2021-06-10 22:28:31)

Add emotes features
From twitch changelog 2021‑06‑08
 - Get Channel Emotes
 - Get Global Emotes
 - Get Emote Sets

From twitch changelog 2021‑06‑07
 - Add Channel Subscription Message EventSub to beta
 - Set Extension Bits Transaction Create EventSub to version 1

From twitch changelog 2021‑06‑03
 - Add is_anonymous to Channel Subscription Gift EventSub


Patch:
diff --git a/README.md b/README.md
index 5f9fcb6..f27bdb8 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,9 @@ If you are looking for the Twitch API docs, see the [Twitch Developer website](h
 - [ ] Get Custom Reward Redemption
 - [ ] Update Custom Reward
 - [ ] Update Redemption Status
+- [x] Get Channel Emotes
+- [x] Get Global Emotes
+- [x] Get Emote Sets
 - [x] Get Channel Chat Badges
 - [x] Get Global Chat Badges
 - [x] Create Clip
diff --git a/chat.go b/chat.go
index 69f6b04..40e44d3 100644
--- a/chat.go
+++ b/chat.go
@@ -57,3 +57,83 @@ func (c *Client) GetGlobalChatBadges() (*GetChatBadgeResponse, error) {
 
 	return channels, nil
 }
+
+// GetChannelEmotesParams ...
+type GetChannelEmotesParams struct {
+	BroadcasterID string `query:"broadcaster_id"`
+}
+
+// GetEmoteSetsParams ...
+type GetEmoteSetsParams struct {
+	EmoteSetIDs []string `query:"emote_set_id"` // Minimum: 1. Maximum: 25.
+}
+
+// GetChannelEmotesResponse ...
+type GetChannelEmotesResponse struct {
+	ResponseCommon
+	Data ManyEmotes
+}
+
+// ManyEmotes ...
+type ManyEmotes struct {
+	Emotes []Emote `json:"data"`
+}
+
+// Emote ...
+type Emote struct {
+	ID     string     `json:"id"`
+	Name   string     `json:"name"`
+	Images EmoteImage `json:"images"`
+	Tier   string     `json:"tier"`
+	Type   string     `json:"emote_type"`
+	Set_ID string     `json:"emote_set_id"`
+}
+
+// EmoteImage ...
+type EmoteImage struct {
+	Url1x string `json:"url_1x"`
+	Url2x string `json:"url_2x"`
+	Url4x string `json:"url_4x"`
+}
+
+// GetChannelChatEmotes ...
+func (c *Client) GetChannelEmotes(params *GetChannelEmotesParams) (*GetChannelEmotesResponse, error) {
+	resp, err := c.get("/chat/emotes", &ManyEmotes{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	emotes := &GetChannelEmotesResponse{}
+	resp.HydrateResponseCommon(&emotes.ResponseCommon)
+	emotes.Data.Emotes = resp.Data.(*ManyEmotes).Emotes
+
+	return emotes, nil
+}
+
+// GetGlobalEmotes ...
+func (c *Client) GetGlobalEmotes() (*GetChannelEmotesResponse, error) {
+	resp, err := c.get("/chat/emotes/global", &ManyEmotes{}, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	emotes := &GetChannelEmotesResponse{}
+	resp.HydrateResponseCommon(&emotes.ResponseCommon)
+	emotes.Data.Emotes = resp.Data.(*ManyEmotes).Emotes
+
+	return emotes, nil
+}
+
+// GetEmoteSets
+func (c *Client) GetEmoteSets(params *GetEmoteSetsParams) (*GetChannelEmotesResponse, error) {
+	resp, err := c.get("/chat/emotes/set", &ManyEmotes{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	emotes := &GetChannelEmotesResponse{}
+	resp.HydrateResponseCommon(&emotes.ResponseCommon)
+	emotes.Data.Emotes = resp.Data.(*ManyEmotes).Emotes
+
+	return emotes, nil
+}
diff --git a/docs/README.md b/docs/README.md
index a5330d7..8f1a3a8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,11 +8,20 @@ Follow the links below to their respective API usage examples:
 - [Authentication](authentication_docs.md)
 - [Bits](bits_docs.md)
 - [Channels](channels_docs.md)
+- [Channels Points](channels_points_docs.md)
+- [Chat](chat_docs.md)
 - [Clips](clips_docs.md)
 - [Entitlement Grants](entitlement_grants_docs.md)
+- [EventSub](eventsub_docs.md)
 - [Extensions](extensions_docs.md)
 - [Games](games_docs.md)
+- [Moderation](moderation_docs.md)
+- [Polls](polls_docs.md)
+- [Prediction](predictions_docs.md)
+- [Stream Markers](stream_markers_docs.md)
 - [Streams](streams_docs.md)
+- [Subscriptions](subscriptions_docs.md)
+- [User Extensions](user_extensions.md)
 - [Users](users_docs.md)
 - [Videos](videos_docs.md)
 - [Webhook Subscriptions](webhook_docs.md)
diff --git a/docs/chat_docs.md b/docs/chat_docs.md
index 277359d..5ab01dd 100644
--- a/docs/chat_docs.md
+++ b/docs/chat_docs.md
@@ -41,3 +41,69 @@ if err != nil {
 
 fmt.Printf("%+v\n", resp)
 ```
+
+## Get Channel Emotes
+
+This is an example of how to get channel emotes
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetChannelEmotes(&helix.GetChannelEmotesParams{
+    BroadcasterID: "145328278",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Get Global Emotes
+
+This is an example of how to get global emotes
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetGlobalEmotes()
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+## Get Emote Sets
+
+This is an example of how to get a set of emotes
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetEmoteSets(&helix.GetEmoteSetsParams{
+    EmoteSetIDs: []string{"300678379"},
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
+
diff --git a/eventsub.go b/eventsub.go
index c0f43bd..0203a6e 100644
--- a/eventsub.go
+++ b/eventsub.go
@@ -83,7 +83,8 @@ const (
 	EventSubTypeChannelFollow                             = "channel.follow"
 	EventSubTypeChannelSubscription                       = "channel.subscribe"
 	EventSubTypeChannelSubscriptionEnd                    = "channel.subscription.end"
-	EventSubTypeChannelSubscriptionGift                   = "channel.subscription.gift" /* beta */
+	EventSubTypeChannelSubscriptionGift                   = "channel.subscription.gift"    /* beta */
+	EventSubTypeChannelSubscriptionMessage                = "channel.subscription.message" /* beta */
 	EventSubTypeChannelCheer                              = "channel.cheer"
 	EventSubTypeChannelRaid                               = "channel.raid"
 	EventSubTypeChannelBan                                = "channel.ban"
@@ -102,7 +103,7 @@ const (
 	EventSubTypeChannelPredictionProgress                 = "channel.prediction.progress"
 	EventSubTypeChannelPredictionLock                     = "channel.prediction.lock"
 	EventSubTypeChannelPredictionEnd                      = "channel.prediction.end"
-	EventSubExtensionBitsTransactionCreate                = "extension.bits_transaction.create" /* beta */
+	EventSubExtensionBitsTransactionCreate                = "extension.bits_transaction.create"
 	EventSubTypeHypeTrainBegin                            = "channel.hype_train.begin"
 	EventSubTypeHypeTrainProgress                         = "channel.hype_train.progress"
 	EventSubTypeHypeTrainEnd                              = "channel.hype_train.end"
@@ -153,6 +154,22 @@ type EventSubChannelSubscriptionGiftEvent struct {
 	Total                int    `json:"total"`
 	Tier                 string `json:"tier"`
 	CumulativeTotal      int    `json:"cumulative_total"`
+	IsAnonymous          bool   `json:"is_anonymous"`
+}
+
+// EventSubChannelSubscriptionMessageEvent
+type EventSubChannelSubscriptionMessageEvent struct {
+	UserID               string          `json:"user_id"`
+	UserLogin            string          `json:"user_login"`
+	UserName             string          `json:"user_name"`
+	BroadcasterUserID    string          `json:"broadcaster_user_id"`
+	BroadcasterUserLogin string          `json:"broadcaster_user_login"`
+	BroadcasterUserName  string          `json:"broadcaster_user_name"`
+	Tier                 string          `json:"tier"`
+	Message              EventSubMessage `json:"message"`
+	CumulativeTotal      int             `json:"cumulative_total"`
+	StreakMonths         int             `json:"streak_months"`
+	DurationMonths       int             `json:"duration_months"`
 }
 
 // Data for a channel cheer notification
@@ -495,6 +512,19 @@ type EventSubReward struct {
 	Prompt string `json:"prompt"`
 }
 
+// EventSubMessage
+type EventSubMessage struct {
+	Text   string          `json:"text"`
+	Emotes []EventSubEmote `json:"emotes"`
+}
+
+// EventSubEmote
+type EventSubEmote struct {
+	Begin int    `json:"begin"`
+	End   int    `json:"end"`
+	ID    string `json:"id"`
+}
+
 // Get all EventSub Subscriptions
 func (c *Client) GetEventSubSubscriptions(params *EventSubSubscriptionsParams) (*EventSubSubscriptionsResponse, error) {
 	resp, err := c.get("/eventsub/subscriptions", &ManyEventSubSubscriptions{}, params)


Test patch:
diff --git a/chat_test.go b/chat_test.go
index 9313007..dfc5a66 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -149,3 +149,222 @@ func TestGetGlobalChatBadges(t *testing.T) {
 		t.Error("expected error does match return error")
 	}
 }
+
+func TestGetChannelEmotes(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode             int
+		options                *Options
+		GetChannelEmotesParams *GetChannelEmotesParams
+		respBody               string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&GetChannelEmotesParams{BroadcasterID: ""},
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&GetChannelEmotesParams{BroadcasterID: "121445595"},
+			`{"data":[{"id":"300678378","name":"scoorfPoko","images":{"url_1x":"https://static-cdn.jtvnw.net/emoticons/v1/300678378/1.0","url_2x":"https://static-cdn.jtvnw.net/emoticons/v1/300678378/2.0","url_4x":"https://static-cdn.jtvnw.net/emoticons/v1/300678378/3.0"},"tier":"1000","emote_type":"subscriptions","emote_set_id":"1347400"}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetChannelEmotes(testCase.GetChannelEmotesParams)
+		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.GetChannelEmotes(&GetChannelEmotesParams{})
+	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 TestGetGlobalEmotes(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		respBody   string
+	}{
+		{
+			http.StatusUnauthorized,
+			&Options{ClientID: "my-client-id"},
+			`{"error":"Unauthorized","status":401,"message":"OAuth token is missing"}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id", UserAccessToken: "my-user-access-token"},
+			`{"data":[{"id":"1220086","name":"TwitchRPG","images":{"url_1x":"https://static-cdn.jtvnw.net/emoticons/v1/1220086/1.0","url_2x":"https://static-cdn.jtvnw.net/emoticons/v1/1220086/2.0","url_4x":"https://static-cdn.jtvnw.net/emoticons/v1/1220086/3.0"}},{"id":"196892","name":"TwitchUnity","images":{"url_1x":"https://static-cdn.jtvnw.net/emoticons/v1/196892/1.0","url_2x":"https://static-cdn.jtvnw.net/emoticons/v1/196892/2.0","url_4x":"https://static-cdn.jtvnw.net/emoticons/v1/196892/3.0"}}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetGlobalEmotes()
+		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.StatusUnauthorized {
+			if resp.Error != "Unauthorized" {
+				t.Errorf("expected error to be %s, got %s", "Unauthorized", resp.Error)
+			}
+
+			if resp.ErrorStatus != http.StatusUnauthorized {
+				t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus)
+			}
+
+			expectedErrMsg := "OAuth token is missing"
+			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.GetGlobalEmotes()
+	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 TestGetEmoteSets(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode         int
+		options            *Options
+		GetEmoteSetsParams *GetEmoteSetsParams
+		respBody           string
+	}{
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			&GetEmoteSetsParams{EmoteSetIDs: nil},
+			`{"error":"Bad Request","status":400,"message":"The parameter \"emote_set_id\" was malformed: the value must be greater than or equal to 1"}`,
+		},
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			&GetEmoteSetsParams{EmoteSetIDs: []string{"300678379"}},
+			`{"data":[{"id":"301147694","name":"sixone3SixDab","images":{"url_1x":"https://static-cdn.jtvnw.net/emoticons/v1/301147694/1.0","url_2x":"https://static-cdn.jtvnw.net/emoticons/v1/301147694/2.0","url_4x":"https://static-cdn.jtvnw.net/emoticons/v1//3.0"},"emote_type":"subscriptions","emote_set_id":"300678379","owner_id":"44931651"}]}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetEmoteSets(testCase.GetEmoteSetsParams)
+		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 := "The parameter \"emote_set_id\" was malformed: the value must be greater than or equal to 1"
+			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.GetEmoteSets(&GetEmoteSetsParams{})
+	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")
+	}
+}